diff --git a/docs/PLATFORM_IDENTITY.md b/docs/PLATFORM_IDENTITY.md new file mode 100644 index 0000000..c1d4b85 --- /dev/null +++ b/docs/PLATFORM_IDENTITY.md @@ -0,0 +1,73 @@ +# Explicit platform identity overrides + +By default, Darling keeps the existing host-derived UUID and `DARLING-` serial. +No override file is generated by installation or launch. This tool imports +values supplied by the user; it does not generate Apple serials or replace OS, +CPU, GPU, or security metadata. + +Preview an OpenCore `config.plist`, a flat GenSMBIOS JSON file, or an existing +`darling-smbios.conf` without changing the selected prefix: + +```sh +python3 tools/configure-darling-platform.py --prefix /absolute/prefix --import /path/to/config.plist +``` + +After reviewing the preview, shut down that prefix and explicitly save: + +```sh +DPREFIX=/absolute/prefix python3 build/runtime/tools/stop-darling-runtime.py +python3 tools/configure-darling-platform.py --prefix /absolute/prefix --import /path/to/config.plist --write +``` + +The default prefix selection is `--prefix`, then `DPREFIX`, then saved +`build/config.json`, then `~/.darling`. Writing refuses a live prefix. The +configuration is activated when that prefix's iokitd starts again; it is an +immutable startup snapshot. An absent or invalid configuration preserves the +existing defaults. Invalid daemon-side configuration is reported once. + +The file is `/private/etc/darling-platform.plist`. It uses this schema: + +```json +{ + "schema": 1, + "properties": { + "IOPlatformSerialNumber": "USER-SUPPLIED", + "model": "UserSuppliedModel" + } +} +``` + +Supported properties: + +| Property | Configuration value | IOKit value | +|---|---|---| +| `IOPlatformSerialNumber` | Printable ASCII string, 1–64 characters | NSString | +| `IOPlatformUUID` | Nonzero UUID; normalized to uppercase | NSString | +| `manufacturer`, `model`, `product-name`, `board-id` | Printable ASCII string, 1–128 characters | NUL-terminated NSData | +| `mlb-serial-number` | Printable ASCII string, 1–64 characters | NUL-terminated NSData | +| `ROM` | Exactly 6 bytes; JSON accepts hexadecimal text | NSData | +| `system-id` | Exactly 16 bytes; JSON accepts hexadecimal text | NSData | + +Supplying a model also supplies the matching product name unless one is +explicitly provided. Supplying a UUID derives its EFI little-endian `system-id`; +a conflicting explicit system ID is rejected. No other missing fields are +invented. These properties belong to the existing platform service; the tool +does not manufacture a firmware/NVRAM registry hierarchy. + +For OpenCore, the importer reads `PlatformInfo/Generic` (or a standalone Generic +dictionary): `SystemProductName`, `SystemSerialNumber`, `SystemUUID`, `MLB`, +and `ROM`. It ignores unrelated boot and security settings. GenSMBIOS aliases +include `Type`, `Serial`, `Board Serial`, and `SmUUID`. Legacy `SMBIOS_*` identity +keys are accepted from `.conf`; parsing never executes shell expressions. + +Existing configured fields survive a partial import. A new explicit UUID +replaces its previously derived system ID. `--set KEY=VALUE` can update an +individual field. Unknown schema versions, unsupported properties, malformed +values, and duplicate JSON fields are rejected. + +Writes are atomic with mode 0600. Repeating the same write changes nothing. The +first previous configuration is kept as +`darling-platform.plist.before-platform-change`, also with mode 0600. Importing +an older `.conf` never edits that source. Symlinks escaping the prefix are +rejected. To restore the original defaults, remove the override file while the +prefix is stopped, then restart it. diff --git a/docs/reviews/pr41-adaptation.md b/docs/reviews/pr41-adaptation.md index 1483097..8748518 100644 --- a/docs/reviews/pr41-adaptation.md +++ b/docs/reviews/pr41-adaptation.md @@ -25,6 +25,8 @@ from main after PRs #43 and #42. It does not claim to resolve account bans. the address of its input pointer. Hardware debug-state support remains unsupported. +The deferred compatibility groups are implemented in the [following stack layer](pr41-deferred-implementation.md). + ## Work left outside this adaptation The original PR's rendered source changes were inspected against its merge base, diff --git a/docs/reviews/pr41-deferred-implementation.md b/docs/reviews/pr41-deferred-implementation.md new file mode 100644 index 0000000..9c2347d --- /dev/null +++ b/docs/reviews/pr41-deferred-implementation.md @@ -0,0 +1,98 @@ +# PR #41 deferred compatibility work + +This follow-up is stacked on the compatibility extraction in PR #44. It +implements the four groups deferred in that review, with explicit configuration +and observable errors rather than automatic identity changes. + +## Shell spawning + +Diagnostics are quiet by default; `DARLING_SHELLSPAWN_DEBUG=1` enables the +existing diagnostic output. The environment and arguments are no longer dumped +on normal launches. + +Stdio sources are duplicated before any standard descriptor is replaced, so +aliases involving descriptors 0–2 are safe. Stream cleanup closes each owned +descriptor once instead of repeatedly closing stream 0. Both fork branches drop +unneeded descriptors, and error paths clean up the exec-status pipe. The +control socket is kept out of the stdio slots. Terminal signal forwarding uses +the remapped stdin. Unrelated descriptors and the configured shell selection +are preserved. + +## CPU frequency + +`hw.cpufrequency` is an unsigned 64-bit frequency in Hz, matching XNU's named +API. Linux cpufreq kHz values are checked for invalid input and overflow before +conversion. The exported host sysfs location is tried as well as `/sys`; an +available native CPUID base-frequency leaf is a fallback. No fixed CPU speed is +invented when no source exists. + +Numeric `HW_CPU_FREQ` retains the legacy 32-bit representation through a +separate compatibility entry, matching XNU's distinction between the modern +named API and the numeric compatibility API. Consumers needing frequencies +above the 32-bit range should use the named 64-bit API. + +## IOKit registry + +- Inline and out-of-line path lookup validate termination, path shape, plane + membership, and length. Paths resolve registered entries with case-sensitive + names; no synthetic firmware nodes are inserted. +- Path output uses inline storage when possible and a bounded sender buffer for + long physical-copy replies. Incoming OOL mappings are released on success and + error paths. The daemon's main queue serializes request processing and reply + copying, so reply storage remains live through the send. +- Plane membership follows registered ancestry. Deleting a parent or child + removes both graph links. Plane-name keys own their strings, and cyclic + registration is refused. +- Raw property access handles strings, data, integer numbers at their reported + widths, and XNU's `Yes`/`No` representation of booleans. It bounds writes by + both the requested count and the fixed MIG array. Missing and unsupported + values return errors; small buffers return IPC errors without writes. + +## Platform configuration + +[Platform identity configuration](../PLATFORM_IDENTITY.md) documents the schema, +OpenCore/GenSMBIOS/legacy imports, startup snapshot, defaults, and rollback. +Neither installation nor launch generates an override file. Existing host-based +identifiers remain stable unless the user explicitly supplies replacements. + +## Validation + +Portable regression checks: + +```sh +python3 -m unittest discover -s tests +python3 scripts/prepare.py --check-only +python3 scripts/validate.py +python3 tools/riot-launcher-offline-check.py +python3 tools/riot-input-offline-check.py +python3 tools/riot-runtime-cleanup-offline-check.py +``` + +The unit tests compile the actual patched fd helper and frequency code. They +exercise fd aliases, unrelated descriptors, repeated failed remaps, conversion +overflow, buffer widths, schema validation, imports, idempotency, backups, +per-prefix isolation, malformed input, and preservation of existing identities. + +With the source patches prepared, build an isolated iokitd candidate and probe: + +```sh +python3 build/runtime/tools/build-riot-registry-compatibility-check.py +darling shell /Volumes/SystemRoot/ABSOLUTE_PROJECT_PATH/build/riot-registry-compatibility-check +``` + +The probe uses the actual daemon objects and MIG handlers, and creates its own +Mach ports. It does not register a daemon or replace the running iokitd. It +checks traversal, deletion, cycles, malformed OOL input, 1,000 direct OOL +lookups, 100 real IPC path round trips, property widths/bounds, and binary +serialization round trips. It also checks daemon-side identity validation. + +For a rebuilt kernel containing the frequency patch: + +```sh +python3 build/runtime/tools/build-riot-cpu-frequency-check.py +darling shell /Volumes/SystemRoot/ABSOLUTE_PROJECT_PATH/build/riot-cpu-frequency-check +``` + +Local validation also ran the fd helper inside Darling and used an isolated +kernel override for the frequency probe. The configured production runtime was +not replaced, no real identity was changed, and no match capture is required. diff --git a/patches/iokitd.patch b/patches/iokitd.patch index 96d4963..c3333e1 100644 --- a/patches/iokitd.patch +++ b/patches/iokitd.patch @@ -26,10 +26,227 @@ index 84161fd..5b23944 100644 src/IOSurface.mm src/IOUserClient.mm diff --git a/src/IORegistryEntry.mm b/src/IORegistryEntry.mm -index 50c10fe..bd501cf 100644 +index 50c10fe..7026e1c 100644 --- a/src/IORegistryEntry.mm +++ b/src/IORegistryEntry.mm -@@ -513,3 +513,86 @@ kern_return_t is_io_registry_entry_get_property_bin +@@ -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; } @@ -116,6 +333,109 @@ index 50c10fe..bd501cf 100644 + 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 @@ -503,13 +823,25 @@ index a568ecc..c2d4063 100644 /* Routine io_ps_copy_chargelog */ diff --git a/src/stubs.c b/src/stubs.c -index 91d11a1..37551e1 100644 +index 91d11a1..5a00091 100644 --- a/src/stubs.c +++ b/src/stubs.c -@@ -45,15 +45,6 @@ kern_return_t is_io_registry_entry_get_property_bytes +@@ -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 @@ -518,11 +850,28 @@ index 91d11a1..37551e1 100644 - STUB(); - return KERN_NOT_SUPPORTED; -} -- + kern_return_t is_io_connect_get_service ( - mach_port_t connection, -@@ -325,20 +316,6 @@ kern_return_t is_io_service_add_notification +@@ -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; } @@ -543,7 +892,7 @@ index 91d11a1..37551e1 100644 kern_return_t is_io_service_acknowledge_notification ( mach_port_t service, -@@ -439,22 +416,6 @@ kern_return_t is_io_object_get_bundle_identifier +@@ -439,22 +397,6 @@ kern_return_t is_io_object_get_bundle_identifier return KERN_NOT_SUPPORTED; } @@ -566,7 +915,7 @@ index 91d11a1..37551e1 100644 kern_return_t is_io_connect_map_memory_into_task ( mach_port_t connection, -@@ -533,20 +494,6 @@ kern_return_t is_io_service_add_notification_64 +@@ -533,20 +475,6 @@ kern_return_t is_io_service_add_notification_64 return KERN_NOT_SUPPORTED; } @@ -587,7 +936,7 @@ index 91d11a1..37551e1 100644 kern_return_t is_io_service_add_notification_ool_64 ( mach_port_t master_port, -@@ -642,18 +589,6 @@ kern_return_t is_io_server_version +@@ -642,18 +570,6 @@ kern_return_t is_io_server_version } @@ -606,7 +955,7 @@ index 91d11a1..37551e1 100644 kern_return_t is_io_service_match_property_table_bin ( mach_port_t service, -@@ -682,22 +617,6 @@ kern_return_t is_io_service_add_notification_bin +@@ -682,48 +598,9 @@ kern_return_t is_io_service_add_notification_bin return KERN_NOT_SUPPORTED; } @@ -625,11 +974,38 @@ index 91d11a1..37551e1 100644 - STUB(); - return KERN_NOT_SUPPORTED; -} -- - kern_return_t is_io_registry_entry_from_path_ool + +-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 ( - mach_port_t master_port, -@@ -736,31 +655,3 @@ 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; } @@ -690,11 +1066,12 @@ index 0000000..38ad635 +#endif diff --git a/src/IOPlatformExpertDevice.mm b/src/IOPlatformExpertDevice.mm new file mode 100644 -index 0000000..e330569 +index 0000000..71b9f4d --- /dev/null +++ b/src/IOPlatformExpertDevice.mm -@@ -0,0 +1,93 @@ +@@ -0,0 +1,98 @@ +#include "IOPlatformExpertDevice.h" ++#include "PlatformIdentity.h" +#import +#import +#import @@ -770,6 +1147,10 @@ index 0000000..e330569 + 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; +} + @@ -3115,3 +3496,111 @@ index 7a6fcfa..26a7d68 100644 + 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 diff --git a/tests/test_platform_identity.py b/tests/test_platform_identity.py new file mode 100644 index 0000000..45602e9 --- /dev/null +++ b/tests/test_platform_identity.py @@ -0,0 +1,157 @@ +"""Disposable-prefix checks for explicit platform identity configuration.""" +import importlib.util +import json +import os +from pathlib import Path +import plistlib +import stat +import subprocess +import sys +import tempfile +import unittest +import uuid + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / 'tools/configure-darling-platform.py' +spec = importlib.util.spec_from_file_location('platform_config', SCRIPT) +platform = importlib.util.module_from_spec(spec) +spec.loader.exec_module(platform) +UUID = '00112233-4455-4677-8899-AABBCCDDEEFF' + + +class PlatformIdentityChecks(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.prefix = self.root / 'prefix with spaces' + + def source(self, name, data): + path = self.root / name + path.write_bytes(data) + return path + + def test_opencore_import_and_uuid_byte_order(self): + source = self.source('OpenCore.plist', plistlib.dumps({'PlatformInfo': {'Generic': { + 'SystemProductName': 'ExampleModel1,1', 'SystemSerialNumber': 'MY-SERIAL', + 'SystemUUID': UUID, 'MLB': 'MY-BOARD', 'ROM': bytes.fromhex('0123456789AB'), + 'SpoofVendor': True}}})) + result = platform.load(source) + self.assertEqual(result['model'], result['product-name']) + self.assertEqual(result['IOPlatformUUID'], UUID) + self.assertEqual(result['system-id'].hex().upper(), '33221100554477468899AABBCCDDEEFF') + self.assertEqual(result['ROM'], bytes.fromhex('0123456789AB')) + self.assertNotIn('SpoofVendor', result) + + def test_gensmbios_partial_import_does_not_generate_missing_identity(self): + source = self.source('identity.json', json.dumps({'Type': 'ExampleModel1,1', 'Serial': 'SUPPLIED'}).encode()) + result = platform.load(source) + self.assertEqual(set(result), {'model', 'product-name', 'IOPlatformSerialNumber'}) + self.assertNotIn('IOPlatformUUID', result) + + def test_invalid_values_are_rejected(self): + for properties in [ + {'IOPlatformUUID': 'not-a-uuid'}, {'IOPlatformUUID': str(uuid.UUID(int=0))}, + {'IOPlatformUUID': UUID, 'system-id': bytes(16)}, {'ROM': bytes(7)}, + {'ROM': 'xyz'}, {'model': 'bad\x00name'}, {'model': 'bad\nname'}, + {'model': 'é'}, {'model': 'X'*129}, {'IOPlatformSerialNumber': ''}, + {'IOPlatformSerialNumber': 123}, {'unknown': 'value'}, {'ROM': True}, + ]: + with self.subTest(properties=properties), self.assertRaises((ValueError, TypeError)): + platform.validate(properties) + + def test_schema_versions_types_and_unknown_fields(self): + for payload in [{'schema': 2, 'properties': {}}, {'schema': True, 'properties': {}}, + {'schema': 1, 'properties': {}, 'extra': 1}, {'schema': 1, 'properties': []}]: + source = self.source('bad.json', json.dumps(payload).encode()) + with self.subTest(payload=payload), self.assertRaises(ValueError): platform.load(source) + source = self.source('huge.json', b' ' * (platform.MAX_SIZE + 1)) + with self.assertRaises(ValueError): platform.load(source) + + def test_legacy_migration_never_executes_or_rewrites_source(self): + marker = self.root / 'executed' + raw = ('SMBIOS_MODEL="ExampleModel1,1"\nSMBIOS_SERIAL_NUMBER="SUPPLIED"\n' + f'SMBIOS_CPU_SPEED="$(touch {marker})"\n').encode() + source = self.source('darling-smbios.conf', raw) + properties = platform.load(source) + self.assertTrue(platform.save(self.prefix, properties)) + self.assertEqual(source.read_bytes(), raw) + self.assertFalse(marker.exists()) + self.assertNotIn('SMBIOS_CPU_SPEED', properties) + + def test_stable_write_backup_permissions_and_roundtrip(self): + first = {'IOPlatformSerialNumber': 'FIRST', 'IOPlatformUUID': UUID} + self.assertTrue(platform.save(self.prefix, first)) + path = platform.destination(self.prefix) + original = path.read_bytes() + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600) + self.assertFalse(platform.save(self.prefix, platform.load(path))) + self.assertEqual(path.read_bytes(), original) + self.assertTrue(platform.save(self.prefix, {'IOPlatformSerialNumber': 'SECOND'})) + self.assertEqual(path.with_name(path.name+'.before-platform-change').read_bytes(), original) + self.assertEqual(platform.load(path), {'IOPlatformSerialNumber': 'SECOND'}) + + def test_prefix_isolation(self): + other = self.root / 'other' + platform.save(self.prefix, {'IOPlatformSerialNumber': 'FIRST'}) + platform.save(other, {'IOPlatformSerialNumber': 'SECOND'}) + self.assertEqual(platform.load(platform.destination(self.prefix))['IOPlatformSerialNumber'], 'FIRST') + self.assertEqual(platform.load(platform.destination(other))['IOPlatformSerialNumber'], 'SECOND') + + def test_no_implicit_migration_without_an_override_file(self): + env = dict(os.environ, DPREFIX=str(self.prefix)) + result = subprocess.run([sys.executable, str(SCRIPT)], env=env, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout)['properties'], {}) + self.assertFalse(self.prefix.exists()) + + def test_preview_and_uuid_replacement_preserve_other_values(self): + platform.save(self.prefix, {'IOPlatformSerialNumber': 'FIRST', 'IOPlatformUUID': UUID}) + replacement = '11112233-4455-4677-8899-AABBCCDDEEFF' + command = [sys.executable, str(SCRIPT), '--prefix', str(self.prefix), '--set', 'IOPlatformUUID='+replacement] + path = platform.destination(self.prefix) + before = path.read_bytes() + preview = subprocess.run(command, capture_output=True, text=True) + self.assertEqual(preview.returncode, 0, preview.stderr) + self.assertEqual(path.read_bytes(), before) + written = subprocess.run([*command, '--write'], capture_output=True, text=True) + self.assertEqual(written.returncode, 0, written.stderr) + result = platform.load(path) + self.assertEqual(result['IOPlatformSerialNumber'], 'FIRST') + self.assertEqual(result['IOPlatformUUID'], replacement) + self.assertEqual(result['system-id'], uuid.UUID(replacement).bytes_le) + + def test_symlink_escape_is_rejected(self): + outside = self.root / 'outside' + outside.mkdir() + (self.prefix / 'private').mkdir(parents=True) + (self.prefix / 'private/etc').symlink_to(outside, target_is_directory=True) + with self.assertRaises(ValueError): platform.save(self.prefix, {}) + self.assertEqual(list(outside.iterdir()), []) + + def test_live_prefix_and_unknown_existing_schema_are_not_overwritten(self): + self.prefix.mkdir() + marker = self.prefix / '.init.pid' + marker.write_text(str(os.getpid())) + with self.assertRaises(ValueError): platform.save(self.prefix, {}) + marker.unlink() + path = platform.destination(self.prefix) + path.parent.mkdir(parents=True) + bad = plistlib.dumps({'schema': 999, 'properties': {}}) + path.write_bytes(bad) + with self.assertRaises(ValueError): platform.save(self.prefix, {}) + self.assertEqual(path.read_bytes(), bad) + + def test_duplicate_json_and_invalid_live_marker_are_rejected(self): + duplicate = self.source('duplicate.json', b'{"schema":1,"schema":2,"properties":{}}') + with self.assertRaises(ValueError): platform.load(duplicate) + self.prefix.mkdir() + (self.prefix / '.init.pid').write_text('0') + with self.assertRaises(ValueError): platform.save(self.prefix, {}) + + def test_malformed_xml_reports_an_error_without_a_traceback(self): + source = self.source('bad.plist', b'') + result = subprocess.run([sys.executable, str(SCRIPT), '--prefix', str(self.prefix), '--import', str(source)], capture_output=True, text=True) + self.assertNotEqual(result.returncode, 0) + self.assertNotIn('Traceback', result.stderr) + self.assertFalse(self.prefix.exists()) diff --git a/tools/build-riot-registry-compatibility-check.py b/tools/build-riot-registry-compatibility-check.py new file mode 100644 index 0000000..e20c2f2 --- /dev/null +++ b/tools/build-riot-registry-compatibility-check.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Build an isolated iokitd candidate and a real-object registry/identity probe.""" +import argparse +from pathlib import Path +import re +import shlex +import subprocess + +root = Path(__file__).resolve().parent.parent +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--source', type=Path, default=root / 'build/darling-src/src/external/iokitd', + help='Prepared iokitd source directory') +args = parser.parse_args() +folder = root / 'build/darling-src/build/src/external/iokitd' +flags = {} +resource = subprocess.check_output(['clang', '-print-resource-dir'], text=True).strip() +for line in (folder / 'CMakeFiles/iokitd.dir/flags.make').read_text().splitlines(): + if line.startswith(('C_', 'CXX_')) and ' = ' in line: + key, value = line.split(' = ', 1) + flags[key] = [re.sub(r'/usr/lib/clang/\d+/include', resource + '/include', value) for value in shlex.split(value)] +link = shlex.split((folder / 'CMakeFiles/iokitd.dir/link.txt').read_text()) + + +def compile(source, output, kind): + subprocess.run(['clang++' if kind == 'CXX' else 'clang', *flags[kind + '_DEFINES'], + '-I' + str(args.source / 'src'), *flags[kind + '_INCLUDES'], *flags[kind + '_FLAGS'], + '-O2', '-c', str(source), '-o', str(output)], check=True) + + +for argument in link[:]: + if argument.startswith('CMakeFiles/iokitd.dir/src/') and argument.endswith('.o'): + relative = argument.split('iokitd.dir/', 1)[1][:-2] + source = args.source / relative + output = root / ('build/registry-candidate-' + source.name + '.o') + compile(source, output, 'CXX' if source.suffix in ('.mm', '.cpp') else 'C') + link[link.index(argument)] = str(output) +link[link.index('-o') + 1] = str(root / 'build/riot-iokitd-candidate') +subprocess.run(link, cwd=folder, check=True) +probe = root / 'build/registry-compatibility-probe.o' +compile(root / 'tools/riot-registry-compatibility-check.mm', probe, 'CXX') +link[link.index(str(root / 'build/registry-candidate-main.mm.o'))] = str(probe) +user = root / 'build/registry-compatibility-client.o' +compile(folder / 'iokitmigUser.c', user, 'C') +link.append(str(user)) +link[link.index('-o') + 1] = str(root / 'build/riot-registry-compatibility-check') +subprocess.run(link, cwd=folder, check=True) +print('Built isolated iokitd candidate and registry compatibility probe') diff --git a/tools/configure-darling-platform.py b/tools/configure-darling-platform.py new file mode 100644 index 0000000..9ba1c87 --- /dev/null +++ b/tools/configure-darling-platform.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Preview or explicitly save per-prefix IOKit platform overrides; defaults stay unchanged.""" +import argparse +import json +import os +from pathlib import Path +import plistlib +import shlex +import tempfile +import uuid +from xml.parsers.expat import ExpatError + +MAX_SIZE = 65536 +STRINGS = {'IOPlatformSerialNumber': 64, 'manufacturer': 128, 'model': 128, + 'product-name': 128, 'board-id': 128, 'mlb-serial-number': 64} +BYTES = {'ROM': 6, 'system-id': 16} +ALIASES = { + 'SystemProductName': 'model', 'Type': 'model', 'SMBIOS_MODEL': 'model', + 'SMBIOS_PRODUCT_NAME': 'product-name', 'SMBIOS_MANUFACTURER': 'manufacturer', + 'SMBIOS_BOARD_ID': 'board-id', 'SystemSerialNumber': 'IOPlatformSerialNumber', + 'Serial': 'IOPlatformSerialNumber', 'SMBIOS_SERIAL_NUMBER': 'IOPlatformSerialNumber', + 'SystemUUID': 'IOPlatformUUID', 'SmUUID': 'IOPlatformUUID', 'SMBIOS_PLATFORM_UUID': 'IOPlatformUUID', + 'MLB': 'mlb-serial-number', 'Board Serial': 'mlb-serial-number', 'SMBIOS_MLB': 'mlb-serial-number', + 'SMBIOS_ROM': 'ROM', 'SMBIOS_SYSTEM_ID': 'system-id', +} + + +def validate(properties): + if not isinstance(properties, dict) or len(properties) > 9: + raise ValueError('properties must be a dictionary of supported identity fields') + result = {} + for key, value in properties.items(): + if key in STRINGS: + if not isinstance(value, str) or not 1 <= len(value) <= STRINGS[key] or any(not 32 <= ord(c) <= 126 for c in value): + raise ValueError('Invalid printable string for ' + key) + elif key == 'IOPlatformUUID': + if not isinstance(value, str): + raise ValueError('IOPlatformUUID must be a UUID string') + parsed = uuid.UUID(value) + if not parsed.int: + raise ValueError('IOPlatformUUID must not be zero') + value = str(parsed).upper() + elif key in BYTES: + if isinstance(value, str): + value = bytes.fromhex(value.replace(':', '').replace('-', '')) + if not isinstance(value, bytes) or len(value) != BYTES[key]: + raise ValueError(key + ' has an invalid byte length') + else: + raise ValueError('Unsupported identity field: ' + str(key)) + result[key] = value + if 'model' in result: + result.setdefault('product-name', result['model']) + if 'IOPlatformUUID' in result: + system_id = uuid.UUID(result['IOPlatformUUID']).bytes_le + if 'system-id' in result and result['system-id'] != system_id: + raise ValueError('system-id does not match IOPlatformUUID') + result['system-id'] = system_id + return result + + +def load(source): + with Path(source).open("rb") as stream: + raw = stream.read(MAX_SIZE + 1) + if len(raw) > MAX_SIZE: + raise ValueError('Identity input exceeds 64 KiB') + if str(source).endswith('.conf'): + payload = {} + for line in raw.decode().splitlines(): + if not line.strip() or line.lstrip().startswith('#'): + continue + key, sep, value = line.partition('=') + if not sep: + raise ValueError('Malformed legacy identity line') + values = shlex.split(value, comments=True) + if len(values) != 1 or key.strip() in payload: + raise ValueError('Malformed or duplicate legacy identity field') + payload[key.strip()] = values[0] + else: + def unique_keys(pairs): + result = {} + for key, value in pairs: + if key in result: raise ValueError('Duplicate JSON identity field: ' + key) + result[key] = value + return result + if raw.lstrip()[:1] in (b'{', b'['): + payload = json.loads(raw, object_pairs_hook=unique_keys) + else: + payload = plistlib.loads(raw) + if not isinstance(payload, dict): + raise ValueError('Identity input must be a dictionary') + if 'schema' in payload: + if set(payload) != {'schema', 'properties'} or type(payload['schema']) is not int or payload['schema'] != 1: + raise ValueError('Unsupported platform identity schema') + return validate(payload['properties']) + if 'PlatformInfo' in payload: + payload = payload['PlatformInfo'].get('Generic') if isinstance(payload['PlatformInfo'], dict) else None + elif 'Generic' in payload: + payload = payload['Generic'] + if not isinstance(payload, dict): + raise ValueError('Missing OpenCore PlatformInfo/Generic dictionary') + result = {} + for key, value in payload.items(): + name = ALIASES.get(key, key) + if name not in {*STRINGS, *BYTES, 'IOPlatformUUID'}: + continue # OpenCore includes unrelated boot, firmware and CPU settings. + if name in result and result[name] != value: + raise ValueError('Conflicting imports for ' + name) + result[name] = value + if not result: + raise ValueError('No supported identity fields found') + return validate(result) + + +def destination(prefix): + prefix = Path(prefix).expanduser() + if not prefix.is_absolute(): + raise ValueError('Prefix must be absolute') + path = prefix / 'private/etc/darling-platform.plist' + if path.is_symlink() or not path.resolve().is_relative_to(prefix.resolve()): + raise ValueError('Identity destination must remain inside the selected prefix') + return path + + +def save(prefix, properties): + path = destination(prefix) + prefix = Path(prefix) + # A host write behind Darling's mounted overlay may not be visible. Never + # stop a user's session implicitly or write a second copy behind its back. + marker = prefix / '.init.pid' + if marker.exists(): + try: + text = marker.read_text().strip() + if not text.isdecimal() or int(text) <= 1: + raise ValueError('Invalid .init.pid; verify the selected prefix is stopped before writing') + pid = int(text) + os.kill(pid, 0) + except ProcessLookupError: + pass + else: + raise ValueError('Shut down the selected Darling prefix before writing platform identity') + encoded = plistlib.dumps({'schema': 1, 'properties': validate(properties)}, sort_keys=True) + if path.exists() and path.read_bytes() == encoded: + return False + path.parent.mkdir(parents=True, exist_ok=True) + # Recheck after creating parents, before opening a temporary file. + path = destination(prefix) + if path.exists(): + load(path) # refuse to silently overwrite an unknown schema + backup = path.with_name(path.name + '.before-platform-change') + if not backup.exists(): + fd = os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(fd, 'wb') as stream: + stream.write(path.read_bytes()) + fd, temporary = tempfile.mkstemp(prefix='.darling-platform-', dir=path.parent) + try: + with os.fdopen(fd, 'wb') as stream: + stream.write(encoded) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + finally: + Path(temporary).unlink(missing_ok=True) + return True + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--prefix', type=Path) + parser.add_argument('--import', dest='source', type=Path, help='OpenCore plist, GenSMBIOS JSON, legacy .conf, or schema-1 plist/JSON') + parser.add_argument('--set', action='append', default=[], metavar='KEY=VALUE') + parser.add_argument('--write', action='store_true', help='Write validated overrides to a stopped prefix (otherwise preview only)') + args = parser.parse_args(argv) + config = Path(__file__).resolve().parents[1] / 'build/config.json' + saved = json.loads(config.read_text()) if config.exists() else {} + prefix = args.prefix or Path(os.environ.get('DPREFIX') or saved.get('prefix', str(Path.home() / '.darling'))) + try: + path = destination(prefix) + properties = load(path) if path.exists() else {} + if args.source: + incoming = load(args.source) + # Replacing a UUID also replaces its previously derived byte form. + if 'IOPlatformUUID' in incoming: + properties.pop('system-id', None) + properties.update(incoming) + for assignment in args.set: + key, sep, value = assignment.partition('=') + if not sep: + raise ValueError('--set requires KEY=VALUE') + if key == 'IOPlatformUUID': properties.pop('system-id', None) + if key == 'model': properties.pop('product-name', None) + properties[key] = value + properties = validate(properties) + if args.write: + changed = save(prefix, properties) + print('Platform identity ' + ('saved' if changed else 'unchanged') + '; restart iokitd to apply.') + else: + print(json.dumps({'schema': 1, 'properties': properties}, default=lambda value: value.hex().upper(), indent=2)) + except (OSError, ValueError, TypeError, plistlib.InvalidFileException, ExpatError) as error: + parser.exit(1, str(error) + '\n') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tools/riot-registry-compatibility-check.mm b/tools/riot-registry-compatibility-check.mm new file mode 100644 index 0000000..ee8d157 --- /dev/null +++ b/tools/riot-registry-compatibility-check.mm @@ -0,0 +1,194 @@ +#import +#include "IORegistryEntry.h" +#include "PlatformIdentity.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include "iokitmigServer.h" +#include "iokitmig.h" +} + +// This probe invokes the real daemon objects and MIG handlers on the main +// thread, without registering a daemon or changing the running iokitd. +mach_port_t g_masterPort, g_deathPort, g_powerManagementPort; +struct Fixture : IORegistryEntry { + NSDictionary* getProperties() override { + return @{@"text": @"hello", @"utf8": @"é", @"small": [NSNumber numberWithUnsignedInt:0x12345678], + @"negative": [NSNumber numberWithInt:-3], @"wide": [NSNumber numberWithUnsignedLongLong:0x123456789abcdef0ULL], + @"flag": @YES, @"float": @1.25, @"array": @[], @"large": [NSMutableData dataWithLength:5000]}; + } +}; +static void checkProperties(Fixture& fixture) { + io_struct_inband_t buffer; mach_msg_type_number_t length; + io_name_t key = "text"; + memset(buffer, 0x7e, sizeof(buffer)); length = 2; + assert(is_io_registry_entry_get_property_bytes(fixture.port(), key, buffer, &length) == kIOReturnIPCError); + assert(length == 2 && buffer[0] == 0x7e); + length = sizeof(buffer); + assert(is_io_registry_entry_get_property_bytes(fixture.port(), key, buffer, &length) == kIOReturnSuccess); + assert(length == 6 && !memcmp(buffer, "hello", 6)); + strcpy(key, "small"); length = sizeof(buffer); + assert(is_io_registry_entry_get_property_bytes(fixture.port(), key, buffer, &length) == kIOReturnSuccess); + uint64_t small = 0; memcpy(&small, buffer, length); assert(length == CFNumberGetByteSize((CFNumberRef)fixture.getProperties()[@"small"]) && small == 0x12345678); + strcpy(key, "negative"); length = sizeof(buffer); + assert(is_io_registry_entry_get_property_bytes(fixture.port(), key, buffer, &length) == kIOReturnSuccess); + int64_t negative = -1; memcpy(&negative, buffer, length); assert(length == CFNumberGetByteSize((CFNumberRef)fixture.getProperties()[@"negative"]) && negative == -3); + strcpy(key, "wide"); length = sizeof(buffer); + assert(is_io_registry_entry_get_property_bytes(fixture.port(), key, buffer, &length) == kIOReturnSuccess); + uint64_t wide; memcpy(&wide, buffer, sizeof(wide)); assert(length == 8 && wide == 0x123456789abcdef0ULL); + strcpy(key, "flag"); length = sizeof(buffer); + assert(is_io_registry_entry_get_property_bytes(fixture.port(), key, buffer, &length) == kIOReturnSuccess); + assert(length == 4 && !memcmp(buffer, "Yes", 4)); + strcpy(key, "utf8"); length = sizeof(buffer); + assert(is_io_registry_entry_get_property_bytes(fixture.port(), key, buffer, &length) == kIOReturnSuccess); + assert(length == 3 && !memcmp(buffer, "\xc3\xa9", 3)); + strcpy(key, "large"); length = UINT32_MAX; memset(buffer, 0x7e, sizeof(buffer)); + assert(is_io_registry_entry_get_property_bytes(fixture.port(), key, buffer, &length) == kIOReturnIPCError); + assert(length == sizeof(buffer) && buffer[0] == 0x7e && buffer[sizeof(buffer)-1] == 0x7e); + for (const char* name : {"float", "array", "missing"}) { + strcpy(key, name); length = sizeof(buffer); + auto expected = !strcmp(name,"missing") ? kIOReturnNoResources : kIOReturnBadArgument; + assert(is_io_registry_entry_get_property_bytes(fixture.port(), key, buffer, &length) == expected && length == 0); + } + memset(key, 'x', sizeof(key)); length = sizeof(buffer); + assert(is_io_registry_entry_get_property_bytes(fixture.port(), key, buffer, &length) == kIOReturnBadArgument && length == 0); +} +static void checkIdentity() { + NSString* uuid = @"00112233-4455-4677-8899-AABBCCDDEEFF"; + auto* valid = DarlingPlatformIdentity::properties(@{@"schema": @1, @"properties": @{@"model": @"ExampleModel1,1", @"IOPlatformUUID": uuid, @"IOPlatformSerialNumber": @"SUPPLIED"}}); + assert(valid && [valid[@"IOPlatformUUID"] isEqual:uuid]); + assert([valid[@"model"] isKindOfClass:[NSData class]] && [valid[@"model"] isEqual:valid[@"product-name"]]); + const unsigned char bytes[] = {0x33,0x22,0x11,0,0x55,0x44,0x77,0x46,0x88,0x99,0xaa,0xbb,0xcc,0xdd,0xee,0xff}; + assert([valid[@"system-id"] isEqual:[NSData dataWithBytes:bytes length:16]]); + for (NSDictionary* invalid in @[ + @{@"schema": @2, @"properties": @{}}, @{@"schema": @YES, @"properties": @{}}, + @{@"schema": @1, @"properties": @{@"unknown": @"value"}}, + @{@"schema": @1, @"properties": @{@"ROM": [NSData data]}}, + @{@"schema": @1, @"properties": @{@"IOPlatformUUID": @"00000000-0000-0000-0000-000000000000"}}, + @{@"schema": @1, @"properties": @{@"IOPlatformUUID": uuid, @"system-id": [NSMutableData dataWithLength:16]}}, + @{@"schema": @1, @"properties": @{@"model": @"bad\nmodel"}} + ]) assert(!DarlingPlatformIdentity::properties(invalid)); + assert(!DarlingPlatformIdentity::load("/private/etc/does-not-exist-platform-test.plist")); + // Exercise the same plist reader used by startup, using only a disposable file. + char filename[] = "/tmp/darling-platform-probe.XXXXXX"; + int descriptor = mkstemp(filename); assert(descriptor >= 0); + NSDictionary* config = @{@"schema": @1, @"properties": @{@"IOPlatformSerialNumber": @"FILE-FIXTURE", @"ROM": [NSData dataWithBytes:"abcdef" length:6]}}; + NSData* encoded = [NSPropertyListSerialization dataWithPropertyList:config format:NSPropertyListXMLFormat_v1_0 options:0 error:NULL]; + assert(encoded); + FILE* output = fdopen(descriptor,"wb"); assert(output); + assert(fwrite([encoded bytes],1,[encoded length],output) == [encoded length]); fclose(output); + NSDictionary* loaded = DarlingPlatformIdentity::load(filename); + assert(loaded && [loaded[@"IOPlatformSerialNumber"] isEqual:@"FILE-FIXTURE"] && [loaded[@"ROM"] length] == 6); + unlink(filename); +} +int main() { + @autoreleasepool { + Fixture fixture; + std::string temporaryPlane = "OwnedPlane"; + fixture.registerInPlane(temporaryPlane.c_str(), "Child", IORegistryEntry::root()); + temporaryPlane.assign(1000, 'x'); + assert(fixture.getPath("OwnedPlane") == "OwnedPlane:/Child"); + assert(IORegistryEntry::fromPath("OwnedPlane:/Child") == &fixture); + assert(IORegistryEntry::fromPath("IOService:/") == IORegistryEntry::root()); + assert(!IORegistryEntry::fromPath("MissingPlane:/")); + assert(!IORegistryEntry::fromPath("OwnedPlane:/child")); + assert(!IORegistryEntry::fromPath("OwnedPlane:/../Child")); + io_name_t plane = "OwnedPlane"; boolean_t inPlane = TRUE; + assert(is_io_registry_entry_in_plane(fixture.port(), plane, &inPlane) == kIOReturnSuccess && inPlane); + strcpy(plane, "Other"); + assert(is_io_registry_entry_in_plane(fixture.port(), plane, &inPlane) == kIOReturnSuccess && !inPlane); + std::vector> entries; + IORegistryEntry* parent = &fixture; + for (unsigned i=0; i<40; ++i) { + auto entry = std::make_unique(); + entry->registerInPlane("OwnedPlane", ("Entry"+std::to_string(i)+std::string(110,'x')).c_str(), parent); + parent = entry.get(); entries.push_back(std::move(entry)); + } + auto longPath = parent->getPath("OwnedPlane"); assert(longPath.size() > sizeof(io_string_inband_t)); + assert(IORegistryEntry::fromPath(longPath) == parent); + // An attempted cycle must not alter an existing root path. + fixture.registerInPlane("OwnedPlane", "cycle", parent); + assert(fixture.getPath("OwnedPlane") == "OwnedPlane:/Child"); + strcpy(plane, "OwnedPlane"); + io_string_t fixed; memset(fixed,'x',sizeof(fixed)); + assert(is_io_registry_entry_get_path(parent->port(), plane, fixed) == kIOReturnNoSpace && fixed[0] == 0); + io_string_inband_t inlinePath = ""; io_buf_ptr_t out = nullptr; mach_msg_type_number_t outCount = 0; + assert(is_io_registry_entry_get_path_ool(parent->port(), plane, inlinePath, &out, &outCount) == kIOReturnSuccess); + assert(!inlinePath[0] && std::string(out,outCount) == longPath && outCount == longPath.size()); + for (unsigned i=0; i<1000; ++i) { + vm_address_t allocation = 0; + assert(vm_allocate(mach_task_self(), &allocation, longPath.size()+1, TRUE) == KERN_SUCCESS); + memcpy((void*)allocation, longPath.c_str(), longPath.size()+1); + kern_return_t result; mach_port_t found; + assert(is_io_registry_entry_from_path_ool(0, inlinePath, (io_buf_ptr_t)allocation, longPath.size()+1, &result, &found) == kIOReturnSuccess); + assert(result == kIOReturnSuccess && found == parent->port()); + } + // Malformed OOL input must fail without publishing a port. + vm_address_t bad = 0; + assert(vm_allocate(mach_task_self(), &bad, 4, TRUE) == KERN_SUCCESS); + memset((void*)bad, 'x', 4); + kern_return_t badResult; mach_port_t badEntry = 123; + assert(is_io_registry_entry_from_path_ool(0, inlinePath, (io_buf_ptr_t)bad, 4, &badResult, &badEntry) == kIOReturnSuccess); + assert(badResult == kIOReturnBadArgument && badEntry == MACH_PORT_NULL); + auto detachedParent = std::make_unique(); + auto detachedChild = std::make_unique(); + detachedParent->registerInPlane("OwnedPlane", "Parent", &fixture); + detachedChild->registerInPlane("OwnedPlane", "Child", detachedParent.get()); + assert(detachedChild->inPlane("OwnedPlane")); + detachedParent.reset(); + assert(!detachedChild->inPlane("OwnedPlane") && detachedChild->getPath("OwnedPlane") == "?unsup_plane?"); + auto deleted = std::make_unique(); + deleted->registerInPlane("OwnedPlane", "Deleted", &fixture); + assert(IORegistryEntry::fromPath("OwnedPlane:/Child/Deleted") == deleted.get()); + deleted.reset(); + assert(!IORegistryEntry::fromPath("OwnedPlane:/Child/Deleted")); + checkProperties(fixture); checkIdentity(); + struct Serializable : IORegistryEntry { + NSDictionary* getProperties() override { return @{@"text": @"value", @"data": [NSData dataWithBytes:"abc" length:3], @"number": @123, @"array": @[@YES, @"x"]}; } + } serializable; + io_buf_ptr_t serialized = nullptr; mach_msg_type_number_t serializedCount = 0; + assert(is_io_registry_entry_get_properties_bin(serializable.port(), &serialized, &serializedCount) == kIOReturnSuccess); + CFStringRef error = nullptr; + CFTypeRef decoded = IOCFUnserializeWithSize(serialized, serializedCount, kCFAllocatorDefault, 0, &error); + assert(decoded && !error && [(NSDictionary*)decoded isEqual:serializable.getProperties()]); + CFRelease(decoded); + vm_deallocate(mach_task_self(), (vm_address_t)serialized, serializedCount); + std::thread([&fixture, parent, &serializable, longPath] { + @autoreleasepool { + io_name_t plane = "OwnedPlane"; + for (unsigned i=0; i<100; ++i) { + io_string_inband_t path = ""; + io_buf_ptr_t bytes = nullptr; mach_msg_type_number_t length = 0; + assert(io_registry_entry_get_path_ool(parent->port(), plane, path, &bytes, &length) == kIOReturnSuccess); + assert(bytes && std::string(bytes,length) == longPath); + vm_deallocate(mach_task_self(), (vm_address_t)bytes, length); + mach_port_t found = 0; kern_return_t result; + assert(io_registry_entry_from_path_ool(fixture.port(), path, const_cast(longPath.c_str()), longPath.size()+1, &result, &found) == kIOReturnSuccess); + assert(result == kIOReturnSuccess && found == parent->port()); + mach_port_deallocate(mach_task_self(), found); + } + io_name_t property = "flag"; io_struct_inband_t raw; mach_msg_type_number_t count = sizeof(raw); + assert(io_registry_entry_get_property_bytes(fixture.port(), property, raw, &count) == kIOReturnSuccess && count == 4 && !memcmp(raw,"Yes",4)); + io_buf_ptr_t bytes = nullptr; count = 0; + assert(io_registry_entry_get_properties_bin(serializable.port(), &bytes, &count) == kIOReturnSuccess); + CFStringRef error = nullptr; + CFTypeRef decoded = IOCFUnserializeWithSize(bytes, count, kCFAllocatorDefault, 0, &error); + assert(decoded && !error && [(NSDictionary*)decoded isEqual:serializable.getProperties()]); + CFRelease(decoded); vm_deallocate(mach_task_self(), (vm_address_t)bytes, count); + puts("REGISTRY_COMPAT paths=1 planes=1 graph_lifetime=1 cycles=1 ool_roundtrips=1000 ipc_roundtrips=100 property_bounds=1 numeric_widths=1 serialization=1 identity_schema=1 pass=1"); + fflush(stdout); + _Exit(0); + } + }).detach(); + dispatch_main(); + } +} diff --git a/upstream/manifest.json b/upstream/manifest.json index 733502b..09e7fa8 100644 --- a/upstream/manifest.json +++ b/upstream/manifest.json @@ -126,7 +126,7 @@ "revision": "5549ac4cd2db923c256f016a1381b3bfb716730a", "repository": "https://github.com/darlinghq/darling-iokitd.git", "patch": "patches/iokitd.patch", - "modified_files": 16, + "modified_files": 17, "new_files": [ "src/IOInterestNotification.h", "src/IOInterestNotification.mm", @@ -138,7 +138,8 @@ "src/LogindSleep.h", "src/PowerSources.mm", "src/HostPowerInhibit.c", - "src/HostPowerInhibit.h" + "src/HostPowerInhibit.h", + "src/PlatformIdentity.h" ] }, {