Implement bounded registry APIs and opt-in platform configuration

This commit is contained in:
lucannez64 committed 2026-10-17 23:32:01 +02:00
1 parent 9fe7ce1f3c
commit b6302bb2c6
9 files changed
+1284 -19

No files matched your search

+73
View File
@@ -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 `<prefix>/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.
+2
View File
@@ -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,
@@ -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.
+506 -17
View File
@@ -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 <stack>
#include <IOKit/IOCFSerialize.h>
+#include <CoreFoundation/CFNumber.h>
#include <cstring>
#include <vector>
#include <IOKit/IOKitLib.h>
#include <IOKit/IOReturn.h>
#include <sstream>
+#include <limits>
+#import <Foundation/NSData.h>
+#import <Foundation/NSNumber.h>
#include "IOIterator.h"
extern "C" {
#include "iokitmigServer.h"
@@ -29,6 +33,12 @@ IORegistryEntry::~IORegistryEntry()
{
std::unique_lock l(m_entriesByIdMutex);
m_entriesById.erase(m_id);
+ // Registry requests and object teardown run on iokitd's main dispatch queue.
+ // Detach both directions so a later traversal cannot follow a deleted entry.
+ for (auto& plane : m_parents)
+ for (auto* parent : plane.second) parent->m_children[plane.first].erase(this);
+ for (auto& plane : m_children)
+ for (auto* child : plane.second) child->m_parents[plane.first].erase(this);
}
IORegistryEntry* IORegistryEntry::findById(uint64_t id)
@@ -79,29 +89,42 @@ std::set<IORegistryEntry*> IORegistryEntry::getChildren(const char* plane)
void IORegistryEntry::registerInPlane(const char* plane, const char* pathName, IORegistryEntry* parent)
{
+ if (!plane || !*plane || !pathName || !*pathName || !parent || parent == this) return;
+ std::set<IORegistryEntry*> visited;
+ std::vector<IORegistryEntry*> pending { parent };
+ while (!pending.empty()) {
+ auto* current = pending.back(); pending.pop_back();
+ if (current == this) return; // refuse cycles, including indirect ones
+ if (!visited.insert(current).second) continue;
+ for (auto* ancestor : current->getParents(plane)) pending.push_back(ancestor);
+ }
m_name[plane] = pathName;
+ if (parent == root()) parent->m_name.emplace(plane, "Root");
auto it = m_parents.find(plane);
if (it == m_parents.end())
- it = m_parents.insert(std::make_pair(std::string_view(plane), std::set<IORegistryEntry*>())).first;
+ it = m_parents.insert(std::make_pair(std::string(plane), std::set<IORegistryEntry*>())).first;
it->second.insert(parent);
it = parent->m_children.find(plane);
if (it == parent->m_children.end())
- it = parent->m_children.insert(std::make_pair(std::string_view(plane), std::set<IORegistryEntry*>())).first;
+ it = parent->m_children.insert(std::make_pair(std::string(plane), std::set<IORegistryEntry*>())).first;
it->second.insert(this);
}
std::string IORegistryEntry::getPath(const char* plane)
{
+ if (!inPlane(plane)) return "?unsup_plane?";
std::stack<std::string> components;
+ std::set<IORegistryEntry*> visited;
IORegistryEntry* cur = this;
while (true)
{
+ if (!visited.insert(cur).second) return "?unsup_plane?";
std::set<IORegistryEntry*> parents = cur->getParents(plane);
if (parents.empty())
{
@@ -113,18 +136,22 @@ std::string IORegistryEntry::getPath(const char* plane)
components.push(cur->getName(plane));
- IORegistryEntry* parent = *parents.begin();
+ IORegistryEntry* parent = nullptr;
+ for (auto* candidate : parents) if (candidate->inPlane(plane)) { parent = candidate; break; }
+ if (!parent) return "?unsup_plane?";
cur = parent;
}
std::string path = plane;
path += ":";
+ if (components.empty()) path += "/";
while (!components.empty())
{
path += "/";
path += components.top();
components.pop();
+ if (path.size() > 65535) return "?unsup_plane?";
}
return path;
@@ -166,11 +193,15 @@ kern_return_t is_io_registry_entry_get_path
io_string_t path
)
{
+ if (!path) return kIOReturnBadArgument;
IORegistryEntry* e = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
if (!e)
return kIOReturnBadArgument;
+ if (!plane || !memchr(plane, 0, sizeof(io_name_t)) || !e->inPlane(plane)) return kIOReturnBadArgument;
std::string mypath = e->getPath(plane);
+ if (mypath == "?unsup_plane?") return kIOReturnBadArgument;
+ if (mypath.size() >= sizeof(io_string_t)) { path[0] = 0; return kIOReturnNoSpace; }
strlcpy(path, mypath.c_str(), sizeof(io_string_t));
return kIOReturnSuccess;
@@ -386,55 +417,59 @@ kern_return_t is_io_registry_entry_get_location_in_plane
return KERN_NOT_SUPPORTED;
}
-kern_return_t is_io_registry_entry_from_path
-(
- mach_port_t master_port,
- io_string_t path,
- mach_port_t *registry_entry
-)
+bool IORegistryEntry::inPlane(const char* plane) const
{
- *registry_entry = 0;
-
- std::vector<std::string> components;
- std::istringstream iss(path);
-
- for (std::string token; std::getline(iss, token, '/'); )
- {
- if (!token.empty())
- components.push_back(std::move(token));
+ if (!plane || !*plane || m_name.find(plane) == m_name.end()) return false;
+ std::set<const IORegistryEntry*> visited;
+ std::vector<const IORegistryEntry*> pending { this };
+ while (!pending.empty()) {
+ auto* current = pending.back(); pending.pop_back();
+ if (current == root()) return true;
+ if (!visited.insert(current).second) continue;
+ auto parents = current->m_parents.find(plane);
+ if (parents != current->m_parents.end())
+ for (auto* parent : parents->second) pending.push_back(parent);
}
-
- if (components.empty())
- return kIOReturnBadArgument;
-
- std::string& planeName = components[0];
- if (planeName[planeName.length()-1] != ':')
- return kIOReturnBadArgument;
- else
- planeName.resize(planeName.length()-1); // remove the final colon
-
- IORegistryEntry* current = IORegistryEntry::root();
- for (size_t i = 1; i < components.size(); i++)
- {
- auto children = current->getChildren(planeName.c_str());
- IORegistryEntry* next = nullptr;
+ return false;
+}
- for (IORegistryEntry* e : children)
- {
- if (strcasecmp(e->getName(planeName.c_str()).c_str(), components[i].c_str()) == 0)
- {
- next = e;
- break;
- }
+IORegistryEntry* IORegistryEntry::fromPath(std::string_view path)
+{
+ if (path.empty() || path.size() > 65535 || path.find('\0') != std::string_view::npos) return nullptr;
+ auto colon = path.find(':');
+ if (colon == 0 || colon == std::string_view::npos || colon >= sizeof(io_name_t)) return nullptr;
+ std::string plane(path.substr(0, colon));
+ if (plane.find('/') != std::string::npos) return nullptr;
+ auto* current = root();
+ if (!current->inPlane(plane.c_str())) return nullptr;
+ path.remove_prefix(colon + 1);
+ if (!path.empty() && path.front() != '/') return nullptr;
+ while (!path.empty()) {
+ path.remove_prefix(1);
+ auto slash = path.find('/');
+ auto name = path.substr(0, slash);
+ if (!name.empty()) {
+ if (name == "." || name == "..") return nullptr;
+ IORegistryEntry* next = nullptr;
+ for (auto* child : current->getChildren(plane.c_str()))
+ if (child->getName(plane.c_str()) == name) { next = child; break; }
+ if (!next) return nullptr;
+ current = next;
}
-
- if (!next)
- return kIOReturnNoDevice;
- current = next;
+ if (slash == std::string_view::npos) break;
+ path.remove_prefix(slash);
}
+ return current; // borrowed until this main-queue request completes
+}
- *registry_entry = current->port();
-
+kern_return_t is_io_registry_entry_from_path(mach_port_t master_port, io_string_t path, mach_port_t* registry_entry)
+{
+ if (!registry_entry) return kIOReturnBadArgument;
+ *registry_entry = MACH_PORT_NULL;
+ if (!path || !memchr(path, 0, sizeof(io_string_t))) return kIOReturnBadArgument;
+ auto* entry = IORegistryEntry::fromPath(path);
+ if (!entry) return kIOReturnNoDevice;
+ *registry_entry = entry->port();
return kIOReturnSuccess;
}
@@ -513,3 +548,189 @@ kern_return_t is_io_registry_entry_get_property_bin
return kr;
}
@@ -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<IORegistryEntry*>(IOObject::lookup(registry_entry));
+ if (!entry || !plane || !memchr(plane, 0, sizeof(io_name_t)) || !entry->inPlane(plane)) return kIOReturnBadArgument;
+ auto value = entry->getPath(plane);
+ if (value == "?unsup_plane?") return kIOReturnBadArgument;
+ if (value.size() < sizeof(io_string_inband_t)) {
+ strlcpy(path, value.c_str(), sizeof(io_string_inband_t));
+ } else {
+ // MIG uses physicalcopy with deallocate=false. The main queue completes
+ // each reply before its next request, so one bounded reusable buffer
+ // keeps the sender's bytes alive without leaking one VM allocation/call.
+ static std::string reply;
+ reply.assign(value);
+ *path_ool = const_cast<char*>(reply.data());
+ *path_oolCnt = reply.size(); // IOKit creates a CFString from bytes, excluding NUL
+ }
+ return kIOReturnSuccess;
+}
+
+kern_return_t is_io_registry_entry_in_plane(mach_port_t registry_entry, io_name_t plane, boolean_t* inPlane)
+{
+ if (!inPlane) return kIOReturnBadArgument;
+ *inPlane = FALSE;
+ auto* entry = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
+ if (!entry || !plane || !memchr(plane, 0, sizeof(io_name_t))) return kIOReturnBadArgument;
+ *inPlane = entry->inPlane(plane);
+ return kIOReturnSuccess;
+}
+
+kern_return_t is_io_registry_entry_get_property_bytes(mach_port_t registry_entry, io_name_t property_name,
+ io_struct_inband_t data, mach_msg_type_number_t* dataCnt)
+{
+ if (!dataCnt) return kIOReturnBadArgument;
+ size_t capacity = std::min<size_t>(*dataCnt, sizeof(io_struct_inband_t));
+ *dataCnt = 0;
+ auto* entry = dynamic_cast<IORegistryEntry*>(IOObject::lookup(registry_entry));
+ if (!entry || !property_name || !memchr(property_name, 0, sizeof(io_name_t))) return kIOReturnBadArgument;
+ NSString* key = [NSString stringWithUTF8String:property_name];
+ if (!key) return kIOReturnBadArgument;
+ id value = [entry->getProperties() objectForKey:key];
+ if (!value) return kIOReturnNoResources;
+ const void* bytes = nullptr;
+ size_t length = 0;
+ uint64_t number = 0;
+ if ([value isKindOfClass:[NSData class]]) {
+ bytes = [value bytes]; length = [value length];
+ } else if ([value isKindOfClass:[NSString class]]) {
+ bytes = [value UTF8String];
+ if (!bytes) return kIOReturnBadArgument;
+ length = strlen((const char*)bytes) + 1;
+ } else if ([value isKindOfClass:[NSNumber class]] && CFGetTypeID((CFTypeRef)value) == CFBooleanGetTypeID()) {
+ bytes = [value boolValue] ? "Yes" : "No";
+ length = [value boolValue] ? sizeof("Yes") : sizeof("No");
+ } else if ([value isKindOfClass:[NSNumber class]]) {
+ const char* type = [value objCType];
+ switch (type ? type[0] : 0) {
+ case 'c': case 'C': case 'B': length = 1; break;
+ case 's': case 'S': length = 2; break;
+ case 'i': case 'I': length = 4; break;
+ case 'l': case 'L': length = sizeof(long); break;
+ case 'q': case 'Q': length = 8; break;
+ default: return kIOReturnBadArgument;
+ }
+ number = [value unsignedLongLongValue]; bytes = &number;
+ } else return kIOReturnBadArgument;
+ if (length > std::numeric_limits<mach_msg_type_number_t>::max()) return kIOReturnNoSpace;
+ if (length > capacity) { *dataCnt = capacity; return kIOReturnIPCError; }
+ *dataCnt = length;
+ if (length && !data) { *dataCnt = 0; return kIOReturnBadArgument; }
+ if (length) memcpy(data, bytes, length);
+ return kIOReturnSuccess;
+}
diff --git a/src/IOSurfaceRoot.mm b/src/IOSurfaceRoot.mm
index d7e6fab..d860582 100644
--- a/src/IOSurfaceRoot.mm
@@ -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 <Foundation/NSString.h>
+#import <Foundation/NSData.h>
+#import <Foundation/NSArray.h>
@@ -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<IORegistryEntry*> getParents(const char* plane);
std::set<IORegistryEntry*> getChildren(const char* plane);
std::string getName(const char* plane = nullptr);
@@ -32,8 +34,8 @@ public:
void registerInPlane(const char* plane, const char* pathName, IORegistryEntry* parent);
protected:
// The key is the plane name, e.g. kIOServicePlane (as declared in IOKit/IOKitKeys.h)
- std::unordered_map<std::string_view, std::string> m_name;
- std::unordered_map<std::string_view, std::set<IORegistryEntry*>> m_parents, m_children;
+ std::unordered_map<std::string, std::string> m_name;
+ std::unordered_map<std::string, std::set<IORegistryEntry*>> m_parents, m_children;
uint64_t m_id;
static std::unordered_map<uint64_t, IORegistryEntry*> m_entriesById;
diff --git a/src/PlatformIdentity.h b/src/PlatformIdentity.h
new file mode 100644
index 0000000..1f83b65
--- /dev/null
+++ b/src/PlatformIdentity.h
@@ -0,0 +1,78 @@
+#ifndef DARLING_PLATFORM_IDENTITY_H
+#define DARLING_PLATFORM_IDENTITY_H
+#import <Foundation/Foundation.h>
+#include <uuid/uuid.h>
+#include <cstdio>
+#include <cstring>
+#include <array>
+
+namespace DarlingPlatformIdentity {
+inline bool printable(id value, size_t maximum) {
+ if (![value isKindOfClass:[NSString class]]) return false;
+ const char* text = [value UTF8String];
+ if (!text) return false;
+ size_t length = strlen(text);
+ if (!length || length > maximum || length != [value length]) return false;
+ for (size_t i = 0; i < length; ++i) if ((unsigned char)text[i] < 32 || (unsigned char)text[i] > 126) return false;
+ return true;
+}
+
+// Validate the entire schema before exposing any overrides. Strings on device
+// tree properties are NUL-terminated NSData; serial/UUID retain NSString types.
+inline NSDictionary* properties(id configuration) {
+ if (![configuration isKindOfClass:[NSDictionary class]] || [configuration count] != 2) return nil;
+ id schema = [configuration objectForKey:@"schema"];
+ if (![schema isKindOfClass:[NSNumber class]] || CFGetTypeID((CFTypeRef)schema) == CFBooleanGetTypeID() ||
+ strchr("fd", [schema objCType][0]) || [schema longLongValue] != 1) return nil;
+ id source = [configuration objectForKey:@"properties"];
+ if (![source isKindOfClass:[NSDictionary class]] || [source count] > 9) return nil;
+ NSMutableDictionary* result = [NSMutableDictionary dictionary];
+ for (id key in source) {
+ if (![key isKindOfClass:[NSString class]]) return nil;
+ id value = [source objectForKey:key];
+ if ([key isEqual:@"IOPlatformUUID"]) {
+ if (!printable(value, 36) || [value length] != 36) return nil;
+ uuid_t uuid;
+ if (uuid_parse([value UTF8String], uuid) || uuid_is_null(uuid)) return nil;
+ uuid_string_t text; uuid_unparse_upper(uuid, text);
+ result[key] = [NSString stringWithUTF8String:text];
+ unsigned char bytes[16]; memcpy(bytes, uuid, sizeof(bytes));
+ for (int i = 0; i < 4; ++i) bytes[i] = uuid[3-i];
+ bytes[4] = uuid[5]; bytes[5] = uuid[4]; bytes[6] = uuid[7]; bytes[7] = uuid[6];
+ NSData* systemID = [NSData dataWithBytes:bytes length:16];
+ if (source[@"system-id"] && ![source[@"system-id"] isEqual:systemID]) return nil;
+ result[@"system-id"] = systemID;
+ } else if ([key isEqual:@"ROM"] || [key isEqual:@"system-id"]) {
+ if (![value isKindOfClass:[NSData class]] || [value length] != ([key isEqual:@"ROM"] ? 6 : 16)) return nil;
+ result[key] = value;
+ } else if ([key isEqual:@"IOPlatformSerialNumber"]) {
+ if (!printable(value, 64)) return nil;
+ result[key] = value;
+ } else if ([key isEqual:@"manufacturer"] || [key isEqual:@"model"] || [key isEqual:@"product-name"] ||
+ [key isEqual:@"board-id"] || [key isEqual:@"mlb-serial-number"]) {
+ if (!printable(value, [key isEqual:@"mlb-serial-number"] ? 64 : 128)) return nil;
+ const char* text = [value UTF8String];
+ result[key] = [NSData dataWithBytes:text length:strlen(text)+1];
+ } else return nil;
+ }
+ if (result[@"model"] && !result[@"product-name"]) result[@"product-name"] = result[@"model"];
+ return result;
+}
+
+inline NSDictionary* load(const char* path) {
+ FILE* file = fopen(path, "rb");
+ if (!file) return nil; // no overrides is the normal, unchanged default
+ std::array<unsigned char, 65537> buffer;
+ size_t size = fread(buffer.data(), 1, buffer.size(), file);
+ bool failed = ferror(file); fclose(file);
+ if (!failed && size <= 65536) {
+ NSData* data = [NSData dataWithBytes:buffer.data() length:size];
+ id config = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:NULL];
+ NSDictionary* overrides = properties(config);
+ if (overrides) return overrides;
+ }
+ fprintf(stderr, "iokitd: invalid darling-platform.plist; retaining default platform properties\n");
+ return nil;
+}
+}
+#endif
+157
View File
@@ -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'<?xml version="1.0"?><broken>')
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())
@@ -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')
+204
View File
@@ -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())
+194
View File
@@ -0,0 +1,194 @@
#import <Foundation/Foundation.h>
#include "IORegistryEntry.h"
#include "PlatformIdentity.h"
#include <IOKit/IOReturn.h>
#include <IOKit/IOKitKeys.h>
#include <IOKit/IOCFUnserialize.h>
#include <mach/mach.h>
#include <cassert>
#include <cstring>
#include <cstdio>
#include <memory>
#include <vector>
#include <thread>
#include <dispatch/dispatch.h>
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<std::unique_ptr<IORegistryEntry>> entries;
IORegistryEntry* parent = &fixture;
for (unsigned i=0; i<40; ++i) {
auto entry = std::make_unique<IORegistryEntry>();
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<IORegistryEntry>();
auto detachedChild = std::make_unique<IORegistryEntry>();
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<IORegistryEntry>();
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<char*>(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();
}
}
+3 -2
View File
@@ -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"
]
},
{