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 #include +#include #include #include #include #include #include +#include +#import +#import #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::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 visited; + std::vector 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())).first; + it = m_parents.insert(std::make_pair(std::string(plane), std::set())).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())).first; + it = parent->m_children.insert(std::make_pair(std::string(plane), std::set())).first; it->second.insert(this); } std::string IORegistryEntry::getPath(const char* plane) { + if (!inPlane(plane)) return "?unsup_plane?"; std::stack components; + std::set visited; IORegistryEntry* cur = this; while (true) { + if (!visited.insert(cur).second) return "?unsup_plane?"; std::set 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(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 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 visited; + std::vector 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(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(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(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(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(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(*dataCnt, sizeof(io_struct_inband_t)); + *dataCnt = 0; + auto* entry = dynamic_cast(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::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 #include #include #include 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 +#import +#import +#import +#import +#import +#include +#include +#include +#include +#include +#include + +// 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 powerSupplies(const char* wantedType) +{ + std::vector 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 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 #include #include +#include +#include +#include #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()); + 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 +#import +#import +#import +#import +#include +#include +#include +#include + +// 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 . +*/ + +#ifndef IOKITD_IOINTERESTNOTIFICATION_H +#define IOKITD_IOINTERESTNOTIFICATION_H +#include "IOObject.h" +#include +#include +#include + +class IOService; + +// The object returned by IOServiceAddInterestNotification() (the kernel's +// IOServiceMessageUserNotification). It lives until the client releases it, +// and sends service messages to the client's notification port in the same +// layout as the kernel, which IODispatchCalloutFromMessage() decodes. +class IOInterestNotification : public IOObject +{ +public: + // Takes over the send right to wakePort. reference holds referenceSize bytes + // of the client's async reference (io_async_ref64_t or io_async_ref_t). + IOInterestNotification(IOService* service, const char* interestType, mach_port_t wakePort, + const void* reference, size_t referenceSize, bool client64); + ~IOInterestNotification(); + + const char* className() const override; + + const std::string& interestType() const { return m_interestType; } + IOService* service() const { return m_service; } + + // Queues messageType without blocking; retries in order when the port is full. + kern_return_t deliver(uint32_t messageType, uint64_t argument); +private: + kern_return_t sendMessage(uint32_t messageType, uint64_t argument); + kern_return_t flushMessages(); + void scheduleRetry(); + bool wakePortIsAlive() const; + struct Message { uint32_t type; uint64_t argument; }; + std::deque m_messages; + bool m_retryScheduled = false; + bool m_failed = false; + IOService* m_service; + std::string m_interestType; + mach_port_t m_wakePort; + OSAsyncReference64 m_reference; + size_t m_referenceSize; + bool m_client64; +}; + +#endif diff --git a/src/IOInterestNotification.mm b/src/IOInterestNotification.mm new file mode 100644 index 0000000..1aa41a9 --- /dev/null +++ b/src/IOInterestNotification.mm @@ -0,0 +1,286 @@ +/* + This file is part of Darling. + + Copyright (C) 2026 Darling developers + + Darling is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Darling is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Darling. If not, see . +*/ + +#include "IOInterestNotification.h" +#include "IOService.h" +#include "iokitd.h" +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "iokitmigServer.h" +} + +// Mirrors xnu's IOServiceMessageUserNotification::PingMsg: the provider is +// passed as a port descriptor, followed by the notification header, the +// client's async reference and the message content. +struct InterestMessageHead +{ + mach_msg_header_t header; + mach_msg_body_t body; + mach_msg_port_descriptor_t provider; + mach_msg_size_t size; + natural_t type; +}; + +// The kernel's kIOUserNotifyMaxMessageSize, reported in the header's size field. +static const mach_msg_size_t kMaxMessageArgumentSize = 64; + +// IODispatchCalloutFromMessage() finds the notification header right after the descriptors. +static_assert(offsetof(InterestMessageHead, size) == sizeof(mach_msg_header_t) + sizeof(mach_msg_body_t) + + sizeof(mach_msg_port_descriptor_t), "notification header must follow the port descriptor"); +static_assert(offsetof(OSNotificationHeader64, reference) == 2 * sizeof(natural_t), "unexpected header layout"); +static_assert(sizeof(IOServiceInterestContent64) == sizeof(natural_t) + sizeof(io_user_reference_t), + "unexpected interest content layout"); + +IOInterestNotification::IOInterestNotification(IOService* service, const char* interestType, + mach_port_t wakePort, const void* reference, size_t referenceSize, bool client64) +: m_service(service), m_interestType(interestType), m_wakePort(wakePort), + m_referenceSize(std::min(referenceSize, sizeof(m_reference))), m_client64(client64) +{ + std::memset(m_reference, 0, sizeof(m_reference)); + std::memcpy(m_reference, reference, m_referenceSize); + m_service->addInterest(this); +} + +IOInterestNotification::~IOInterestNotification() +{ + m_service->removeInterest(this); + if (MACH_PORT_VALID(m_wakePort)) + mach_port_deallocate(mach_task_self(), m_wakePort); +} + +const char* IOInterestNotification::className() const +{ + return "IOServiceMessageUserNotification"; +} + +kern_return_t IOInterestNotification::sendMessage(uint32_t messageType, uint64_t argument) +{ + // Header, reference (at most OSAsyncReference64) and IOServiceInterestContent64 + alignas(8) uint8_t buffer[sizeof(InterestMessageHead) + sizeof(OSAsyncReference64) + + sizeof(IOServiceInterestContent64)]; + std::memset(buffer, 0, sizeof(buffer)); + + InterestMessageHead* head = reinterpret_cast(buffer); + head->header.msgh_bits = MACH_MSGH_BITS_COMPLEX + | MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, MACH_MSG_TYPE_MAKE_SEND); + head->header.msgh_remote_port = m_wakePort; + // Received as msgh_remote_port: the client checks that it still holds this notification. + head->header.msgh_local_port = port(); + head->header.msgh_id = kOSNotificationMessageID; + + head->body.msgh_descriptor_count = 1; + head->provider.name = m_service->port(); + head->provider.disposition = MACH_MSG_TYPE_MAKE_SEND; + head->provider.type = MACH_MSG_PORT_DESCRIPTOR; + + // A pointer-sized argument: 8 bytes for 64-bit clients, 4 otherwise. + // The low bits of the type would carry (size & 3), which is 0 for both. + size_t argumentSize = m_client64 ? sizeof(io_user_reference_t) : sizeof(uint32_t); + head->size = kMaxMessageArgumentSize + sizeof(IOServiceInterestContent64); + head->type = kIOServiceMessageNotificationType; + + uint8_t* p = buffer + sizeof(InterestMessageHead); + std::memcpy(p, m_reference, m_referenceSize); + p += m_referenceSize; + + natural_t type = messageType; + std::memcpy(p, &type, sizeof(type)); + p += sizeof(type); + if (m_client64) + std::memcpy(p, &argument, sizeof(argument)); + else + { + uint32_t argument32 = (uint32_t) argument; + std::memcpy(p, &argument32, sizeof(argument32)); + } + p += argumentSize; + + head->header.msgh_size = (mach_msg_size_t) (p - buffer); + + // Never block iokitd on a client that does not drain its port. + kern_return_t kr = mach_msg(&head->header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, head->header.msgh_size, + 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); + + if (kr != KERN_SUCCESS) + { + // A pseudo-receive on failed delivery returns newly copied rights. + // mach_msg_destroy handles the remote right/descriptors, but not local. + mach_msg_type_name_t local = MACH_MSGH_BITS_LOCAL(head->header.msgh_bits); + if (local == MACH_MSG_TYPE_MOVE_SEND || local == MACH_MSG_TYPE_MOVE_SEND_ONCE) + mach_port_deallocate(mach_task_self(), head->header.msgh_local_port); + mach_msg_destroy(&head->header); + } + return kr; +} + +bool IOInterestNotification::wakePortIsAlive() const +{ + mach_port_type_t type = 0; + return mach_port_type(mach_task_self(), m_wakePort, &type) == KERN_SUCCESS + && (type & MACH_PORT_TYPE_SEND); +} + +void IOInterestNotification::scheduleRetry() +{ + if (m_retryScheduled) + return; + m_retryScheduled = true; + mach_port_t notificationPort = port(); + // Do not retain a send right while waiting: deregistration must still + // destroy this object. Look it up again on the same serial queue. + IOInterestNotification* expected = this; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + auto current = dynamic_cast(IOObject::lookup(notificationPort)); + if (current != expected) + return; + current->m_retryScheduled = false; + current->flushMessages(); + }); +} + +kern_return_t IOInterestNotification::flushMessages() +{ + while (!m_messages.empty()) + { + kern_return_t kr = MACH_SEND_INVALID_DEST; + if (wakePortIsAlive()) + { + const auto& message = m_messages.front(); + kr = sendMessage(message.type, message.argument); + // Mach can report success if the destination died during the send. + if (!wakePortIsAlive()) + kr = MACH_SEND_INVALID_DEST; + } + if (kr == MACH_SEND_TIMED_OUT || kr == MACH_SEND_INTERRUPTED || kr == MACH_SEND_NO_BUFFER) + { + scheduleRetry(); + return KERN_SUCCESS; // accepted; preserve message order until the port drains + } + if (kr != KERN_SUCCESS) + { + m_failed = true; + m_messages.clear(); + os_log_error(OS_LOG_DEFAULT, "iokitd: notification destination unavailable: 0x%x", kr); + mach_port_t notificationPort = port(); + IOInterestNotification* expected = this; + // Defer removal until tellApps has finished building its pending set. + dispatch_async(dispatch_get_main_queue(), ^{ + auto current = dynamic_cast(IOObject::lookup(notificationPort)); + if (current == expected) + current->m_service->removeInterest(current); + }); + return kr; + } + m_messages.pop_front(); + } + return KERN_SUCCESS; +} + +kern_return_t IOInterestNotification::deliver(uint32_t messageType, uint64_t argument) +{ + if (m_failed) + return MACH_SEND_INVALID_DEST; + m_messages.push_back({messageType, argument}); + return flushMessages(); +} + +static kern_return_t addInterestNotification(mach_port_t service, const char* interestType, + mach_port_t wakePort, const void* reference, size_t referenceSize, bool client64, + mach_port_t* notification) +{ + *notification = MACH_PORT_NULL; + + // On failure dispatch_mig_server destroys the request, including wakePort. + // Only a successful registration takes ownership of that send right. + IOService* s = dynamic_cast(IOObject::lookup(service)); + if (!s) + return kIOReturnBadArgument; + if (!MACH_PORT_VALID(wakePort)) + return kIOReturnBadArgument; + if (!s->acceptsInterest(interestType)) + return kIOReturnUnsupported; + + IOInterestNotification* n; + try + { + n = new IOInterestNotification(s, interestType, wakePort, reference, referenceSize, client64); + } + catch (const std::exception& e) + { + os_log_error(OS_LOG_DEFAULT, "iokitd: cannot create interest notification: %s", e.what()); + return kIOReturnNoResources; + } + + *notification = n->port(); + // The reply carries a copy of the send right; drop ours afterwards. + n->releaseLater(); + return kIOReturnSuccess; +} + +kern_return_t is_io_service_add_interest_notification_64 +( + mach_port_t service, + io_name_t type_of_interest, + mach_port_t wake_port, + io_async_ref64_t reference, + mach_msg_type_number_t referenceCnt, + mach_port_t *notification +) +{ + if (referenceCnt > kOSAsyncRef64Count) + { + *notification = MACH_PORT_NULL; + return kIOReturnBadArgument; + } + + OSAsyncReference64 zreference = {}; + std::memcpy(zreference, reference, referenceCnt * sizeof(zreference[0])); + + return addInterestNotification(service, type_of_interest, wake_port, zreference, + sizeof(zreference), true, notification); +} + +kern_return_t is_io_service_add_interest_notification +( + mach_port_t service, + io_name_t type_of_interest, + mach_port_t wake_port, + io_async_ref_t reference, + mach_msg_type_number_t referenceCnt, + mach_port_t *notification +) +{ + if (referenceCnt > kOSAsyncRefCount) + { + *notification = MACH_PORT_NULL; + return kIOReturnBadArgument; + } + + io_async_ref_t zreference = {}; + std::memcpy(zreference, reference, referenceCnt * sizeof(zreference[0])); + + return addInterestNotification(service, type_of_interest, wake_port, zreference, + sizeof(zreference), false, notification); +} diff --git a/src/IOObject.cpp b/src/IOObject.cpp index 1a40da6..232f150 100644 --- a/src/IOObject.cpp +++ b/src/IOObject.cpp @@ -50,12 +50,17 @@ IOObject::IOObject() throw std::runtime_error("Failed to create dispatch source"); dispatch_source_set_event_handler(m_dispatchSource, ^{ - dispatch_mig_server(m_dispatchSource, is_iokit_subsystem.maxsize, iokit_server); + dispatch_mig_server(m_dispatchSource, is_iokit_subsystem.maxsize, + [](mach_msg_header_t* request, mach_msg_header_t* reply) -> boolean_t { + if (request->msgh_id == MACH_NOTIFY_NO_SENDERS) + return IOObject::deathNotify(request, reply); + return iokit_server(request, reply); + }); }); dispatch_resume(m_dispatchSource); mach_port_t oldTargetOfNotification = MACH_PORT_NULL; - kr = mach_port_request_notification(task, m_port, MACH_NOTIFY_NO_SENDERS, 1, g_deathPort, MACH_MSG_TYPE_MAKE_SEND_ONCE, &oldTargetOfNotification); + kr = mach_port_request_notification(task, m_port, MACH_NOTIFY_NO_SENDERS, 1, m_port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &oldTargetOfNotification); if (kr != KERN_SUCCESS) throw std::runtime_error("Failed to setup MACH_NOTIFY_NO_SENDERS"); @@ -64,12 +69,15 @@ IOObject::IOObject() IOObject::~IOObject() { - if (m_dispatchSource) - dispatch_release(m_dispatchSource); - m_objects.erase(m_port); - - mach_port_deallocate(mach_task_self(), m_port); + // A receive source must be cancelled before its receive right is destroyed. + // The cancellation handler captures the port value, never the dying object. + mach_port_t port = m_port; + dispatch_source_set_cancel_handler(m_dispatchSource, ^{ + mach_port_destroy(mach_task_self(), port); + }); + dispatch_source_cancel(m_dispatchSource); + dispatch_release(m_dispatchSource); } void IOObject::retain() @@ -113,12 +121,30 @@ boolean_t IOObject::deathNotify(mach_msg_header_t *request, mach_msg_header_t *r if (Request->not_header.msgh_id == MACH_NOTIFY_NO_SENDERS) { - IOObject* object = lookup(request->msgh_remote_port); - - if (object != nullptr) + // No-senders messages carry no object name in their body. They are + // delivered to the object's own receive right, identified by local_port. + mach_port_t port = request->msgh_local_port; + dispatch_async(dispatch_get_main_queue(), ^{ + IOObject* object = lookup(port); + if (!object) + return; + mach_port_status_t status; + mach_msg_type_number_t count = MACH_PORT_RECEIVE_STATUS_COUNT; + if (mach_port_get_attributes(mach_task_self(), port, MACH_PORT_RECEIVE_STATUS, + (mach_port_info_t) &status, &count) != KERN_SUCCESS) + return; + if (status.mps_srights) + { + // A new send right was made after this notification was queued. + mach_port_t old = MACH_PORT_NULL; + mach_port_request_notification(mach_task_self(), port, MACH_NOTIFY_NO_SENDERS, + status.mps_mscount, port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &old); + if (MACH_PORT_VALID(old)) + mach_port_deallocate(mach_task_self(), old); + return; + } delete object; - else - os_log_error(OS_LOG_DEFAULT, "Received MACH_NOTIFY_NO_SENDERS for port %d, which matches no objects", request->msgh_remote_port); + }); Reply->Head.msgh_bits = 0; Reply->Head.msgh_remote_port = MACH_PORT_NULL; diff --git a/src/IOPMrootDomain.h b/src/IOPMrootDomain.h new file mode 100644 index 0000000..9f9fc95 --- /dev/null +++ b/src/IOPMrootDomain.h @@ -0,0 +1,89 @@ +/* + This file is part of Darling. + + Copyright (C) 2026 Darling developers + + Darling is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Darling is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Darling. If not, see . +*/ + +#ifndef IOKITD_IOPMROOTDOMAIN_H +#define IOKITD_IOPMROOTDOMAIN_H + +#include "IOUserClient.h" +#include "ServiceRegistry.h" +#include + +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 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 . +*/ + +#include "IOPMrootDomain.h" +#include "IOInterestNotification.h" +#include "LogindSleep.h" +#include +#include +#include +#include +#include +#include +#include +#include +#import + +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 #include +#include +#include + +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 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 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 +#include +#include #import 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 IOService::interests(const char* type) const +{ + std::vector 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(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(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 . +*/ + +#include "LogindSleep.h" +#include +#include +#include +#include +#include +#include + +#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 . +*/ + +/* + * 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 + +#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 +#include +#include +#include + +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 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 #include #include +#include #include #include #import @@ -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 AppAssertions::m_instances; static std::unordered_map 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 #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 getParents(const char* plane); std::set 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 m_name; - std::unordered_map> m_parents, m_children; + std::unordered_map m_name; + std::unordered_map> m_parents, m_children; uint64_t m_id; static std::unordered_map 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 +#include +#include +#include +#include + +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 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