mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
Add stable opt-in per-prefix identity provisioning
This commit is contained in:
1 parent
b6302bb2c6
commit
91eaf21175
7 files changed
+418
-39
No files matched your search
@@ -1,9 +1,9 @@
|
||||
# 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.
|
||||
No override file is generated by installation or launch. Import mode uses values supplied by the user. Explicit `--provision` mode can
|
||||
generate synthetic identifiers; it does not issue authentic Apple identities
|
||||
or change 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:
|
||||
@@ -42,6 +42,7 @@ Supported properties:
|
||||
| Property | Configuration value | IOKit value |
|
||||
|---|---|---|
|
||||
| `IOPlatformSerialNumber` | Printable ASCII string, 1–64 characters | NSString |
|
||||
| `provisioning_UDID` | UUID string matching `IOPlatformUUID` | 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 |
|
||||
@@ -71,3 +72,65 @@ first previous configuration is kept as
|
||||
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.
|
||||
|
||||
## Opt-in provisioning
|
||||
|
||||
Preview a fresh or repaired identity without writing anything:
|
||||
|
||||
```sh
|
||||
python3 tools/configure-darling-platform.py --prefix /absolute/prefix --provision
|
||||
```
|
||||
|
||||
Stop the prefix, then save the identity and prefix metadata:
|
||||
|
||||
```sh
|
||||
DPREFIX=/absolute/prefix python3 build/runtime/tools/stop-darling-runtime.py
|
||||
python3 tools/configure-darling-platform.py --prefix /absolute/prefix --provision --write
|
||||
```
|
||||
|
||||
Launch Darling normally afterwards. This requires the updated iokitd from this
|
||||
PR; the config tool alone cannot update an older running daemon. Provisioning
|
||||
is never called automatically by installation or launch.
|
||||
|
||||
Missing serials use 12 characters and missing MLBs use 17 characters, sampled
|
||||
with Python's `secrets` module from `0123456789ABCDEFGHJKLMNPQRSTUVWXYZ` (Base-34).
|
||||
These are synthetic strings, with no Apple model, factory, date or checksum
|
||||
validation, allocation, or guarantee of acceptance by Apple services. Missing
|
||||
UUIDs use RFC4122 v4; `system-id` contains the UUID's 16 little-endian bytes.
|
||||
For this project `provisioning_UDID` is the uppercase platform UUID alias, not
|
||||
a separate Apple provisioning credential or a claim about hardware UDIDs.
|
||||
|
||||
Missing ROMs default to six random bytes with the local/unicast bits set. You
|
||||
can explicitly supply a three-byte OUI using `--rom-oui AA:BB:CC` (replace the
|
||||
example with your chosen unicast OUI). It only affects a missing ROM. An OUI
|
||||
does not make a generated identity Apple-issued.
|
||||
|
||||
Provisioning replaces the known shared serial `C02ZL0GVMD6N` and UUID
|
||||
`6F3B2A1E-8C4D-4E5F-9A7B-1D2C3E4F5A6B`, along with the latter's derived fields.
|
||||
It preserves other existing custom values, including imported serials and UUIDs
|
||||
that do not follow the generated formats. A standalone custom `system-id` is
|
||||
preserved by deriving its UUID instead of generating a different one. Model,
|
||||
board and manufacturer values are only set when supplied by the user.
|
||||
|
||||
The command reads an existing prefix `darling-smbios.conf` for migration, then
|
||||
applies the canonical plist, an explicit import, and `--set` values in that
|
||||
order. It writes:
|
||||
|
||||
- `private/etc/darling-platform.plist`: canonical runtime overrides.
|
||||
- `private/etc/darling-smbios.conf`: matching shell-quoted `SMBIOS_*` values,
|
||||
including `SMBIOS_PROVISIONING_UDID`; unrelated legacy settings are not exported.
|
||||
- `System/Library/CoreServices/SystemVersion.plist`: an exact copy of the selected
|
||||
runtime's metadata, not a fabricated macOS release. The runtime root defaults
|
||||
to the saved SDK path or `/usr/local/libexec/darling`; override with
|
||||
`--runtime-root /path/to/runtime`.
|
||||
|
||||
A valid runtime version file is required. The command checks all destinations
|
||||
before changing identity files, rejects live prefixes and escaping symlinks,
|
||||
backs up each previous file once with `.before-platform-change`, and replaces
|
||||
each file atomically. The set of three files is not a single transaction: the
|
||||
canonical identity is saved first so rerunning after interruption completes
|
||||
provisioning with the same identity. Concurrent CLI writers serialize through
|
||||
a per-prefix lock. Repeated writes preserve identifiers and unchanged files.
|
||||
Previewing a fresh identity generates a sample; only `--write` makes it stable.
|
||||
To restore host-derived defaults after provisioning, remove both identity files
|
||||
while stopped, so a later provisioning run cannot reimport the legacy copy.
|
||||
@@ -53,7 +53,8 @@ above the 32-bit range should use the named 64-bit API.
|
||||
[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.
|
||||
identifiers remain stable unless the user explicitly supplies replacements or
|
||||
requests synthetic per-prefix provisioning.
|
||||
|
||||
## Validation
|
||||
|
||||
@@ -96,3 +97,12 @@ darling shell /Volumes/SystemRoot/ABSOLUTE_PROJECT_PATH/build/riot-cpu-frequency
|
||||
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.
|
||||
|
||||
|
||||
Opt-in provisioning now fills missing synthetic serial/MLB, UUID, system-id,
|
||||
provisioning UUID alias and ROM fields. It repairs the two known shared defaults
|
||||
while retaining custom values, synchronizes the canonical and legacy config,
|
||||
and copies actual runtime SystemVersion metadata. Disposable-prefix tests cover
|
||||
concurrent writers, idempotency, migration, backups, live-prefix refusal, and
|
||||
preflight failures. The registry probe validates all ten allowed properties.
|
||||
See [platform identity usage](../PLATFORM_IDENTITY.md#opt-in-provisioning).
|
||||
@@ -3522,10 +3522,10 @@ index 90155ed..e955bd4 100644
|
||||
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
|
||||
index 0000000..6c87959
|
||||
--- /dev/null
|
||||
+++ b/src/PlatformIdentity.h
|
||||
@@ -0,0 +1,78 @@
|
||||
@@ -0,0 +1,81 @@
|
||||
+#ifndef DARLING_PLATFORM_IDENTITY_H
|
||||
+#define DARLING_PLATFORM_IDENTITY_H
|
||||
+#import <Foundation/Foundation.h>
|
||||
@@ -3553,17 +3553,18 @@ index 0000000..1f83b65
|
||||
+ 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;
|
||||
+ if (![source isKindOfClass:[NSDictionary class]] || [source count] > 10) 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 ([key isEqual:@"IOPlatformUUID"] || [key isEqual:@"provisioning_UDID"]) {
|
||||
+ 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];
|
||||
+ if ([key isEqual:@"provisioning_UDID"]) continue;
|
||||
+ 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];
|
||||
@@ -3583,6 +3584,8 @@ index 0000000..1f83b65
|
||||
+ result[key] = [NSData dataWithBytes:text length:strlen(text)+1];
|
||||
+ } else return nil;
|
||||
+ }
|
||||
+ if (result[@"provisioning_UDID"] && result[@"IOPlatformUUID"] &&
|
||||
+ ![result[@"provisioning_UDID"] isEqual:result[@"IOPlatformUUID"]]) return nil;
|
||||
+ if (result[@"model"] && !result[@"product-name"]) result[@"product-name"] = result[@"model"];
|
||||
+ return result;
|
||||
+}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Provisioning contracts using disposable prefixes and runtime metadata."""
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
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('provisioning', SCRIPT)
|
||||
platform = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(platform)
|
||||
|
||||
|
||||
class ProvisioningChecks(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
self.prefix = self.root / 'prefix with spaces'
|
||||
self.runtime = self.root / 'runtime with spaces'
|
||||
self.version = self.runtime / 'System/Library/CoreServices/SystemVersion.plist'
|
||||
self.version.parent.mkdir(parents=True)
|
||||
self.metadata = {'ProductName': 'Darling', 'ProductVersion': '11.7.4', 'ProductBuildVersion': 'Darling-test'}
|
||||
self.version.write_bytes(plistlib.dumps(self.metadata))
|
||||
self.command = [sys.executable, str(SCRIPT), '--prefix', str(self.prefix),
|
||||
'--provision', '--runtime-root', str(self.runtime)]
|
||||
|
||||
def run_cli(self, *args):
|
||||
result = subprocess.run([*self.command, *args], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
return result
|
||||
|
||||
def test_generated_shapes_uuid_byte_order_and_prefix_independence(self):
|
||||
values = [platform.provision({}) for _ in range(20)]
|
||||
for value in values:
|
||||
for key, length in [('IOPlatformSerialNumber', 12), ('mlb-serial-number', 17)]:
|
||||
self.assertEqual(len(value[key]), length)
|
||||
self.assertTrue(set(value[key]) <= set(platform.BASE34))
|
||||
parsed = uuid.UUID(value['IOPlatformUUID'])
|
||||
self.assertEqual(parsed.version, 4)
|
||||
self.assertEqual(parsed.variant, uuid.RFC_4122)
|
||||
self.assertEqual(value['system-id'], parsed.bytes_le)
|
||||
self.assertEqual(value['provisioning_UDID'], str(parsed).upper())
|
||||
self.assertEqual(len(value['ROM']), 6)
|
||||
self.assertEqual(value['ROM'][0] & 3, 2)
|
||||
self.assertEqual(len({v['IOPlatformUUID'] for v in values}), len(values))
|
||||
|
||||
def test_preview_does_not_create_prefix(self):
|
||||
preview = json.loads(self.run_cli().stdout)
|
||||
self.assertIn('provisioning_UDID', preview['properties'])
|
||||
self.assertFalse(self.prefix.exists())
|
||||
|
||||
def test_write_three_files_idempotent_and_importable(self):
|
||||
self.run_cli('--write')
|
||||
canonical = platform.destination(self.prefix)
|
||||
legacy = self.prefix / 'private/etc/darling-smbios.conf'
|
||||
version = self.prefix / 'System/Library/CoreServices/SystemVersion.plist'
|
||||
self.assertEqual(platform.load(canonical), platform.load(legacy))
|
||||
self.assertEqual(version.read_bytes(), self.version.read_bytes())
|
||||
files = [canonical, legacy, version]
|
||||
snapshot = [(f.read_bytes(), f.stat().st_mtime_ns) for f in files]
|
||||
self.assertIn('unchanged', self.run_cli('--write').stdout)
|
||||
self.assertEqual(snapshot, [(f.read_bytes(), f.stat().st_mtime_ns) for f in files])
|
||||
for f in [canonical, legacy]:
|
||||
self.assertEqual(f.stat().st_mode & 0o777, 0o600)
|
||||
|
||||
def test_known_defaults_replaced_and_custom_values_preserved(self):
|
||||
legacy = self.prefix / 'private/etc/darling-smbios.conf'
|
||||
legacy.parent.mkdir(parents=True)
|
||||
original = (f'SMBIOS_SERIAL_NUMBER={platform.KNOWN_SERIAL}\n'
|
||||
f'SMBIOS_PLATFORM_UUID={platform.KNOWN_UUID}\n'
|
||||
'SMBIOS_MLB=CUSTOM-BOARD\nSMBIOS_MODEL=CustomModel\nSMBIOS_ROM=001122334455\n').encode()
|
||||
legacy.write_bytes(original)
|
||||
self.run_cli('--write')
|
||||
result = platform.load(platform.destination(self.prefix))
|
||||
self.assertNotEqual(result['IOPlatformSerialNumber'], platform.KNOWN_SERIAL)
|
||||
self.assertNotEqual(result['IOPlatformUUID'], platform.KNOWN_UUID)
|
||||
self.assertEqual(result['mlb-serial-number'], 'CUSTOM-BOARD')
|
||||
self.assertEqual(result['model'], 'CustomModel')
|
||||
self.assertEqual(result['ROM'], bytes.fromhex('001122334455'))
|
||||
self.assertEqual(legacy.with_name(legacy.name+'.before-platform-change').read_bytes(), original)
|
||||
before = result.copy()
|
||||
self.run_cli('--write')
|
||||
self.assertEqual(platform.load(platform.destination(self.prefix)), before)
|
||||
|
||||
def test_custom_canonical_identity_wins_over_legacy_defaults(self):
|
||||
custom = platform.provision({})
|
||||
platform.save(self.prefix, custom)
|
||||
legacy = self.prefix / 'private/etc/darling-smbios.conf'
|
||||
legacy.write_text(f'SMBIOS_SERIAL_NUMBER={platform.KNOWN_SERIAL}\nSMBIOS_PLATFORM_UUID={platform.KNOWN_UUID}\n')
|
||||
self.run_cli('--write')
|
||||
self.assertEqual(platform.load(platform.destination(self.prefix)), custom)
|
||||
|
||||
def test_explicit_oui_and_existing_rom(self):
|
||||
value = platform.provision({}, bytes.fromhex('001122'))
|
||||
self.assertEqual(value['ROM'][:3], bytes.fromhex('001122'))
|
||||
self.assertEqual(platform.provision(value, bytes.fromhex('AABBCC')), value)
|
||||
for oui in ['1234', '011122', 'garbage']:
|
||||
result = subprocess.run([*self.command, '--rom-oui', oui, '--write'], capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(platform.destination(self.prefix).exists())
|
||||
|
||||
def test_version_source_required_before_identity_written(self):
|
||||
for raw in [b'not a plist', plistlib.dumps({'ProductVersion': '99.0'})]:
|
||||
self.version.write_bytes(raw)
|
||||
result = subprocess.run([*self.command, '--write'], capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(platform.destination(self.prefix).exists())
|
||||
self.assertFalse((self.prefix / 'private/etc/darling-smbios.conf').exists())
|
||||
self.version.unlink()
|
||||
result = subprocess.run([*self.command, '--write'], capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertNotIn('Traceback', result.stderr)
|
||||
|
||||
def test_system_version_backup_and_symlink_preflight(self):
|
||||
target = self.prefix / 'System/Library/CoreServices/SystemVersion.plist'
|
||||
target.parent.mkdir(parents=True)
|
||||
original = plistlib.dumps({'ProductVersion': 'old'})
|
||||
target.write_bytes(original)
|
||||
self.run_cli('--write')
|
||||
self.assertEqual(target.with_name(target.name+'.before-platform-change').read_bytes(), original)
|
||||
self.assertEqual(plistlib.loads(target.read_bytes()), self.metadata)
|
||||
target.unlink()
|
||||
target.symlink_to(self.version)
|
||||
before = platform.destination(self.prefix).read_bytes()
|
||||
result = subprocess.run([*self.command, '--set', 'model=Changed', '--write'], capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertEqual(platform.destination(self.prefix).read_bytes(), before)
|
||||
|
||||
def test_live_prefix_is_not_changed(self):
|
||||
self.prefix.mkdir()
|
||||
(self.prefix / '.init.pid').write_text(str(os.getpid()))
|
||||
result = subprocess.run([*self.command, '--write'], capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(platform.destination(self.prefix).exists())
|
||||
|
||||
def test_concurrent_writers_share_one_identity(self):
|
||||
workers = [subprocess.Popen([*self.command, '--write'], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, text=True) for _ in range(4)]
|
||||
for worker in workers:
|
||||
output, error = worker.communicate(timeout=15)
|
||||
self.assertEqual(worker.returncode, 0, error)
|
||||
canonical = platform.destination(self.prefix)
|
||||
self.assertEqual(platform.load(canonical), platform.load(self.prefix / 'private/etc/darling-smbios.conf'))
|
||||
self.assertFalse(canonical.with_name(canonical.name+'.before-platform-change').exists())
|
||||
|
||||
def test_custom_system_id_is_preserved_and_udid_mismatch_rejected(self):
|
||||
value = platform.provision({})
|
||||
self.assertEqual(platform.provision({'provisioning_UDID': value['provisioning_UDID']})['IOPlatformUUID'], value['IOPlatformUUID'])
|
||||
supplied = {'system-id': value['system-id']}
|
||||
self.assertEqual(platform.provision(supplied)['IOPlatformUUID'], value['IOPlatformUUID'])
|
||||
with self.assertRaises(ValueError):
|
||||
platform.validate({'IOPlatformUUID': value['IOPlatformUUID'], 'provisioning_UDID': str(uuid.uuid4())})
|
||||
@@ -2,6 +2,9 @@
|
||||
"""Preview or explicitly save per-prefix IOKit platform overrides; defaults stay unchanged."""
|
||||
import argparse
|
||||
import json
|
||||
import fcntl
|
||||
import secrets
|
||||
from contextlib import nullcontext
|
||||
import os
|
||||
from pathlib import Path
|
||||
import plistlib
|
||||
@@ -11,6 +14,9 @@ import uuid
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
MAX_SIZE = 65536
|
||||
BASE34 = '0123456789ABCDEFGHJKLMNPQRSTUVWXYZ'
|
||||
KNOWN_SERIAL = 'C02ZL0GVMD6N'
|
||||
KNOWN_UUID = '6F3B2A1E-8C4D-4E5F-9A7B-1D2C3E4F5A6B'
|
||||
STRINGS = {'IOPlatformSerialNumber': 64, 'manufacturer': 128, 'model': 128,
|
||||
'product-name': 128, 'board-id': 128, 'mlb-serial-number': 64}
|
||||
BYTES = {'ROM': 6, 'system-id': 16}
|
||||
@@ -22,23 +28,24 @@ ALIASES = {
|
||||
'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',
|
||||
'SMBIOS_PROVISIONING_UDID': 'provisioning_UDID',
|
||||
}
|
||||
|
||||
|
||||
def validate(properties):
|
||||
if not isinstance(properties, dict) or len(properties) > 9:
|
||||
if not isinstance(properties, dict) or len(properties) > 10:
|
||||
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':
|
||||
elif key in ('IOPlatformUUID', 'provisioning_UDID'):
|
||||
if not isinstance(value, str):
|
||||
raise ValueError('IOPlatformUUID must be a UUID string')
|
||||
raise ValueError(key + ' must be a UUID string')
|
||||
parsed = uuid.UUID(value)
|
||||
if not parsed.int:
|
||||
raise ValueError('IOPlatformUUID must not be zero')
|
||||
raise ValueError(key + ' must not be zero')
|
||||
value = str(parsed).upper()
|
||||
elif key in BYTES:
|
||||
if isinstance(value, str):
|
||||
@@ -55,6 +62,8 @@ def validate(properties):
|
||||
if 'system-id' in result and result['system-id'] != system_id:
|
||||
raise ValueError('system-id does not match IOPlatformUUID')
|
||||
result['system-id'] = system_id
|
||||
if result.get('provisioning_UDID', result['IOPlatformUUID']) != result['IOPlatformUUID']:
|
||||
raise ValueError('provisioning_UDID must match IOPlatformUUID')
|
||||
return result
|
||||
|
||||
|
||||
@@ -101,7 +110,7 @@ def load(source):
|
||||
result = {}
|
||||
for key, value in payload.items():
|
||||
name = ALIASES.get(key, key)
|
||||
if name not in {*STRINGS, *BYTES, 'IOPlatformUUID'}:
|
||||
if name not in {*STRINGS, *BYTES, 'IOPlatformUUID', 'provisioning_UDID'}:
|
||||
continue # OpenCore includes unrelated boot, firmware and CPU settings.
|
||||
if name in result and result[name] != value:
|
||||
raise ValueError('Conflicting imports for ' + name)
|
||||
@@ -111,50 +120,49 @@ def load(source):
|
||||
return validate(result)
|
||||
|
||||
|
||||
def destination(prefix):
|
||||
def checked_path(prefix, relative):
|
||||
prefix = Path(prefix).expanduser()
|
||||
if not prefix.is_absolute():
|
||||
raise ValueError('Prefix must be absolute')
|
||||
path = prefix / 'private/etc/darling-platform.plist'
|
||||
path = prefix / relative
|
||||
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'
|
||||
def destination(prefix):
|
||||
return checked_path(prefix, 'private/etc/darling-platform.plist')
|
||||
|
||||
|
||||
def ensure_stopped(prefix):
|
||||
marker = Path(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)
|
||||
try:
|
||||
os.kill(int(text), 0)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
else:
|
||||
return
|
||||
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:
|
||||
|
||||
|
||||
def atomic_write(path, data):
|
||||
if path.exists() and path.read_bytes() == data:
|
||||
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())
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
fd, temporary = tempfile.mkstemp(prefix='.darling-platform-', dir=path.parent)
|
||||
try:
|
||||
with os.fdopen(fd, 'wb') as stream:
|
||||
stream.write(encoded)
|
||||
stream.write(data)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
os.replace(temporary, path)
|
||||
@@ -163,35 +171,159 @@ def save(prefix, properties):
|
||||
return True
|
||||
|
||||
|
||||
def save(prefix, properties):
|
||||
path = destination(prefix)
|
||||
ensure_stopped(prefix)
|
||||
encoded = plistlib.dumps({'schema': 1, 'properties': validate(properties)}, sort_keys=True)
|
||||
checked_path(prefix, 'private/etc/darling-platform.plist.before-platform-change')
|
||||
if path.exists():
|
||||
load(path) # refuse to silently overwrite an unknown schema
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return atomic_write(destination(prefix), encoded)
|
||||
|
||||
|
||||
def provision(properties, rom_oui=None):
|
||||
"""Fill missing identifiers, replacing only the two known shared defaults."""
|
||||
result = validate(properties)
|
||||
if result.get('IOPlatformSerialNumber') == KNOWN_SERIAL:
|
||||
result.pop('IOPlatformSerialNumber')
|
||||
if result.get('IOPlatformUUID', '').upper() == KNOWN_UUID:
|
||||
result.pop('IOPlatformUUID')
|
||||
result.pop('system-id', None)
|
||||
result.pop('provisioning_UDID', None)
|
||||
for key, length in [('IOPlatformSerialNumber', 12), ('mlb-serial-number', 17)]:
|
||||
if key not in result:
|
||||
result[key] = ''.join(secrets.choice(BASE34) for _ in range(length))
|
||||
if 'IOPlatformUUID' not in result:
|
||||
# Preserve a custom standalone EFI system ID during legacy migration.
|
||||
result['IOPlatformUUID'] = (result['provisioning_UDID'] if 'provisioning_UDID' in result else
|
||||
str(uuid.UUID(bytes_le=result['system-id']) if 'system-id' in result else uuid.uuid4()).upper())
|
||||
result.setdefault('provisioning_UDID', result['IOPlatformUUID'])
|
||||
if 'ROM' not in result:
|
||||
if rom_oui is not None:
|
||||
result['ROM'] = rom_oui + secrets.token_bytes(3)
|
||||
else:
|
||||
value = bytearray(secrets.token_bytes(6))
|
||||
value[0] = (value[0] | 2) & 0xfe # locally administered, unicast
|
||||
result['ROM'] = bytes(value)
|
||||
return validate(result)
|
||||
|
||||
|
||||
def provisioning_files(prefix, properties, runtime_root):
|
||||
"""Preflight every file before making any identity changes."""
|
||||
version = Path(runtime_root) / 'System/Library/CoreServices/SystemVersion.plist'
|
||||
with version.open('rb') as stream:
|
||||
raw = stream.read(MAX_SIZE + 1)
|
||||
if len(raw) > MAX_SIZE:
|
||||
raise ValueError('Runtime SystemVersion.plist exceeds 64 KiB')
|
||||
metadata = plistlib.loads(raw)
|
||||
if not isinstance(metadata, dict) or any(not isinstance(metadata.get(key), str) or not metadata[key]
|
||||
for key in ('ProductName', 'ProductVersion', 'ProductBuildVersion')):
|
||||
raise ValueError('Runtime SystemVersion.plist is missing version metadata')
|
||||
reverse = {value: key for key, value in ALIASES.items() if key.startswith('SMBIOS_')}
|
||||
lines = ['# Synthetic per-prefix identity; generated by configure-darling-platform.py.']
|
||||
for key, value in sorted(properties.items()):
|
||||
value = value.hex().upper() if isinstance(value, bytes) else value
|
||||
lines.append(reverse[key] + '=' + shlex.quote(value))
|
||||
files = {
|
||||
'private/etc/darling-smbios.conf': ('\n'.join(lines) + '\n').encode(),
|
||||
'System/Library/CoreServices/SystemVersion.plist': raw,
|
||||
'private/etc/darling-platform.plist': plistlib.dumps({'schema': 1, 'properties': properties}, sort_keys=True),
|
||||
}
|
||||
for relative in files:
|
||||
path = checked_path(prefix, relative)
|
||||
checked_path(prefix, relative + '.before-platform-change')
|
||||
if path.exists():
|
||||
if relative.endswith('darling-platform.plist'):
|
||||
load(path)
|
||||
elif relative.endswith('.conf'):
|
||||
load(path)
|
||||
else:
|
||||
if not isinstance(plistlib.loads(path.read_bytes()), dict):
|
||||
raise ValueError('Existing SystemVersion.plist is invalid')
|
||||
return files
|
||||
|
||||
|
||||
def save_provisioning(prefix, properties, runtime_root):
|
||||
ensure_stopped(prefix)
|
||||
files = provisioning_files(prefix, properties, runtime_root)
|
||||
# Save the canonical identity first: an interrupted run reuses it on retry.
|
||||
order = ['private/etc/darling-platform.plist', 'private/etc/darling-smbios.conf',
|
||||
'System/Library/CoreServices/SystemVersion.plist']
|
||||
changed = False
|
||||
for relative in order:
|
||||
changed = atomic_write(checked_path(prefix, relative), files[relative]) or changed
|
||||
return changed
|
||||
|
||||
|
||||
def prefix_lock(prefix):
|
||||
ensure_stopped(prefix)
|
||||
path = checked_path(prefix, 'private/etc/.darling-platform.lock')
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
|
||||
stream = os.fdopen(fd, 'r+')
|
||||
fcntl.flock(stream, fcntl.LOCK_EX)
|
||||
return stream
|
||||
|
||||
|
||||
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('--provision', action='store_true', help='Generate missing synthetic identifiers and provision prefix metadata')
|
||||
parser.add_argument('--runtime-root', type=Path, help='Runtime supplying SystemVersion.plist (default: saved SDK or /usr/local/libexec/darling)')
|
||||
parser.add_argument('--rom-oui', help='Explicit three-byte unicast OUI for a missing ROM; default is locally administered')
|
||||
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:
|
||||
prefix = prefix.expanduser()
|
||||
rom_oui = None
|
||||
if args.rom_oui:
|
||||
if not args.provision:
|
||||
raise ValueError('--rom-oui requires --provision')
|
||||
rom_oui = bytes.fromhex(args.rom_oui.replace(':', '').replace('-', ''))
|
||||
if len(rom_oui) != 3 or rom_oui[0] & 1:
|
||||
raise ValueError('--rom-oui requires exactly three unicast bytes')
|
||||
with prefix_lock(prefix) if args.write else nullcontext():
|
||||
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.
|
||||
properties = {}
|
||||
if args.provision:
|
||||
legacy = checked_path(prefix, 'private/etc/darling-smbios.conf')
|
||||
if legacy.exists():
|
||||
properties.update(load(legacy))
|
||||
if path.exists():
|
||||
incoming = load(path)
|
||||
if 'IOPlatformUUID' in incoming:
|
||||
properties.pop('system-id', None)
|
||||
properties.pop('provisioning_UDID', None)
|
||||
properties.update(incoming)
|
||||
if args.source:
|
||||
incoming = load(args.source)
|
||||
if 'IOPlatformUUID' in incoming:
|
||||
properties.pop('system-id', None)
|
||||
properties.pop('provisioning_UDID', 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 == 'IOPlatformUUID':
|
||||
properties.pop('system-id', None)
|
||||
properties.pop('provisioning_UDID', None)
|
||||
if key == 'model': properties.pop('product-name', None)
|
||||
properties[key] = value
|
||||
runtime_root = args.runtime_root or Path(saved.get('sdk', '/usr/local/libexec/darling'))
|
||||
if args.provision:
|
||||
properties = provision(properties, rom_oui)
|
||||
provisioning_files(prefix, properties, runtime_root)
|
||||
else:
|
||||
properties = validate(properties)
|
||||
if args.write:
|
||||
changed = save(prefix, properties)
|
||||
changed = (save_provisioning(prefix, properties, runtime_root) if args.provision else 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))
|
||||
|
||||
@@ -64,12 +64,23 @@ static void checkProperties(Fixture& fixture) {
|
||||
}
|
||||
static void checkIdentity() {
|
||||
NSString* uuid = @"00112233-4455-4677-8899-AABBCCDDEEFF";
|
||||
auto* valid = DarlingPlatformIdentity::properties(@{@"schema": @1, @"properties": @{@"model": @"ExampleModel1,1", @"IOPlatformUUID": uuid, @"IOPlatformSerialNumber": @"SUPPLIED"}});
|
||||
auto* valid = DarlingPlatformIdentity::properties(@{@"schema": @1, @"properties": @{@"model": @"ExampleModel1,1", @"provisioning_UDID": uuid, @"IOPlatformUUID": uuid, @"IOPlatformSerialNumber": @"SUPPLIED"}});
|
||||
assert(valid && [valid[@"IOPlatformUUID"] isEqual:uuid]);
|
||||
assert([valid[@"provisioning_UDID"] isEqual:uuid]);
|
||||
NSMutableDictionary* complete = [@{@"IOPlatformSerialNumber": @"SYNTHETIC123", @"IOPlatformUUID": uuid,
|
||||
@"provisioning_UDID": uuid, @"model": @"ExampleModel", @"product-name": @"ExampleModel",
|
||||
@"manufacturer": @"Example", @"board-id": @"Board", @"mlb-serial-number": @"SYNTHETIC-BOARD",
|
||||
@"ROM": [NSData dataWithBytes:"abcdef" length:6]} mutableCopy];
|
||||
complete[@"system-id"] = valid[@"system-id"];
|
||||
assert([complete count] == 10);
|
||||
assert(DarlingPlatformIdentity::properties(@{@"schema": @1, @"properties": complete}));
|
||||
[complete release];
|
||||
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": @1, @"properties": @{@"provisioning_UDID": @"bad"}},
|
||||
@{@"schema": @1, @"properties": @{@"IOPlatformUUID": uuid, @"provisioning_UDID": @"11112233-4455-4677-8899-AABBCCDDEEFF"}},
|
||||
@{@"schema": @2, @"properties": @{}}, @{@"schema": @YES, @"properties": @{}},
|
||||
@{@"schema": @1, @"properties": @{@"unknown": @"value"}},
|
||||
@{@"schema": @1, @"properties": @{@"ROM": [NSData data]}},
|
||||
|
||||
@@ -9,6 +9,7 @@ import tempfile
|
||||
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
checks = {
|
||||
"riot-registry-compatibility-check": "REGISTRY_COMPAT",
|
||||
"riot-cfhost-cancel-check": "CFHOST_CANCEL_CHECK",
|
||||
"riot-launchservices-null-error-check": "LAUNCHSERVICES_NULL_ERROR",
|
||||
"m327-cgl-resolver-check": "GL_RESOLVER",
|
||||
|
||||
Reference in new issue
Block a user