Files
Acelogic 1510d8c6ce Snapintosh public release
An EL2 monitor/hypervisor and toolchain that boots macOS 27 on the ASUS A16
(Snapdragon X2 Elite), with an Adreno Metal plug-in and IOGPU kext, and
air2ir3, an AIR to Mesa ir3 shader compiler.

MIT, with GPL-2.0 components listed in LICENSE and THIRD_PARTY_NOTICES.md.
No Apple, Qualcomm, ASUS or Microsoft binaries are included.
2026-10-21 21:26:36 -04:00

1635 lines
85 KiB
Python

# SPDX-License-Identifier: MIT
"""Audited guest device placement for portable EL2 register models."""
from dataclasses import dataclass, replace
import hashlib
import struct
from pmgrboot import VIRTUAL_ANE_STATES
MODEL_MAX = 128
RESET_WORDS = (MODEL_MAX+63)//64
MODEL_WORDS = 8
MCC_COHERENT_V1 = 1
MCC_DCS_COHERENT_V1 = 2
AIC3_FLAT_V1 = 3
MCC_SOCNI_V1 = 4
WATCHDOG_WD1_V1 = 5
MCC_PTD_COHERENT_V1 = 6
GPIO_QUIET_V1 = 7
AES_NO_KEYS_V1 = 8
SPMI_GEN3_EMPTY_V1 = 9
DOCK_ABSENT_V1 = 10
GPIO_UART_RX_IDLE_V1 = 11
GPIO_IDLE_INPUTS_V1 = 12
DOCK_FIFO_V1 = 13
SOCNI_ERRORS_V1 = 14
PLATFORM_ERROR_POLICY_V1 = 15
AOP_TIMEBASE_V1 = 16
DART_T8110_V1 = 17
PMGR_ACTIVE_BOOT_V1 = 18
PMGR_CPU0_BOOT_V1 = 1
PMGR_AON_FUSES_V1 = 19
PMGR_CPU_PERF_BOOT_V1 = 20
PMGR_DVMR_WEIGHTS_V1 = 21
PMGR_AON_COUNTER_CONFIG_V1 = 22
PMGR_AP_WAKE_MASK_V1 = 23
PTD_IRQ_CONFIG_V1 = 24
PTD_PMP_ACTIVE_V1 = 25
PMGR_CPU_CLUSTER_ACTIVE_V1 = 26
PMGR_SOC_GATE_POLICY_V1 = 27
PMGR_SOC_DPE_POLICY_V1 = 28
PMGR_AXI2AF_POLICY_V1 = 29
PMC_ENABLE_POLICY_V1 = 30
PMC_RMBS_REQUEST_V1 = 31
PMC_DVMR_CONFIG_V1 = 32
PMGR_AVE_TVM_IDLE_V1 = 33
PMGR_SOC_PERF_RESTORE_V1 = 34
PMGR_ACC_PERF_RESTORE_V1 = 35
PMGR_ENERGY_RESTORE_V1 = 36
PMGR_ACC_BOOT_POLICY_V1 = 37
PMGR_LLT_DISABLED_V1 = 38
CORESIGHT_LOCK_V1 = 39
GPIO_IDLE_PADS_V2 = 40
QSPI_BOOT_V1 = 41
DWI_STARTUP_V1 = 42
I2C_EMPTY_V1 = 43
WATCHDOG_SMC_DISARM_V1 = 44
SEP_ASC_MAILBOX_V1 = 45
AES_IDLE_V1 = 46
AES_SOFTWARE_V1 = 47
SPMI_PMU_RTC_V1 = 48
PMU_RTC_CONTRACT = 0xf715f704f802000e
# Virtual ANS2 (compat/ans.h): the ASC mailbox pages of /arm-io/ans reg[0] and
# the NVMe BAR (reg[3]). config[0] is the monitor-private reservation (state,
# one reserved page, then the RAM disk at ANS_DISK_OFFSET), outside the guest.
ANS_ASC_V1 = 49
ANS_NVME_V1 = 50
ANS_ASC_WINDOW = (0x389600000, 0x9000)
ANS_NVME_WINDOW = (0x38dcc0000, 0x60000)
ANS_DISK_OFFSET = 0x34000
ANS_DISK_MAX = 1 << 30
# The reserved page (compat/ans.h ANS_FW_OFFSET): zero, outside stage 2, never
# served by the model. It is the virtual IOP's only firmware segment.
ANS_FW_OFFSET = 0x30000
ANS_FW_BYTES = 0x4000
ANS_IRQS = (1010, 1007, 1039) # outbox (interrupts[2]), inbox empty ([1]), NVMe ([4])
PMU_RTC_LIMIT_US = (1 << 32)*1000000-1
PMGR_AVE_CLUSTER_V1 = 1
# (logical PS register offset, workaround base, mapping mode), in the
# pinned AppleT8140PMGR table's order. Mode affects mapping, not policy bits.
AXI2AF_BANKS_V1 = (
(0x3e8,0x408000000,0),(0x280,0x413000000,1),(0x288,0x417000000,0),
(0x278,0x38b000000,0),(0x270,0x380000000,0),(0x240,0x284000000,0),
(0x248,0x288000000,0),(0x1f0,0x481000000,0),(0x230,0x28c000000,0),
(0x2a0,0x396000000,0),
)
PMP_ACTIVE_MASK = 0xffb80
DART_SIO_BOOT_V1 = 44 | (1 << 8)
DART_SEP_BOOT_V1 = 44 | (2 << 8)
DART_ANE_INIT_V1 = 44 | (3 << 8)
DART_ADRENO_BOOT_V1 = 44 | (4 << 8)
# The generated /arm-io/dart-adreno (probes/adreno.py): dart-sio's properties
# with its own name, reg, interrupt, phandle, low-window mode and window. Pinned like the
# firmware nodes above so a change to either side is refused, not absorbed.
ADRENO_DART_FINGERPRINT = 'd8ddd5218435bbd8d0cac187d74fed2a8d4193b13ca017752e3da028e243dd8c'
# The same node with the ADRENO_VA_WIDE window (vm-size 0x3_C000_0000; agents/gpu-va-wide), pinned on its own:
# xnuboot.configure_adreno_va_wide sets ADRENO_DART_WIDE, and each launch admits only its own node.
ADRENO_DART_FINGERPRINT_WIDE = 'ae14e764141872f0cfe7652f10eb879e60ec12bfb90b15802b436cbe3471a41d'
ADRENO_DART_WIDE = False
def _dart_boot_policy(dt, device):
policies={
'/device-tree/arm-io/dart-sio': ('SIO',DART_SIO_BOOT_V1,16,((0x385060000,0x20000),),
'bf2fe3cc29c084894652917ba2ffabdc327905ffecde809a00d3922aa5309bec'),
'/device-tree/arm-io/dart-sep': ('SEP',DART_SEP_BOOT_V1,3,((0x280b80000,0x20000),),
'2fbea55ebfea208ee215a03d81f2097066381b1f73bb4d29003cf169e88fa922'),
'/device-tree/arm-io/dart-ane': ('ANE',DART_ANE_INIT_V1,16,
((0x401800000,0xc000),(0x401820000,0x20000),
(0x401840000,0x20000),(0x401810000,0x4000)),
'864851bde49d3eda35abd10610b98eaa06fd5742fc0bf5d4c0417d1db6aee6b3'),
}
if device.path == '/device-tree/arm-io/dart-adreno':
# Generated by probes/adreno.py from dart-sio's property set; the
# fingerprint below is of that generated node (tools/test-adreno-image.py).
policies[device.path] = ('Adreno', DART_ADRENO_BOOT_V1, 16, ((0x710000000, 0x20000),),
ADRENO_DART_FINGERPRINT_WIDE if ADRENO_DART_WIDE else ADRENO_DART_FINGERPRINT)
if device.path not in policies:
return 44
label,policy,streams,windows,expected=policies[device.path]
# Includes the complete property set, so added bypass, no-sleep, reset,
# APF, firmware mappings or performance policies cannot silently opt in.
digest=hashlib.sha256()
for key in sorted(dt.nodes[device.path]):
if key == 'dart-id':
continue
if policy==DART_ANE_INIT_V1 and key=='allow-dram-apf-slices-0':
continue # Derived from chosen/dram-base and checked below.
prop=dt.nodes[device.path][key]
if prop.placeholder:
raise ValueError(f'{label} DART boot policy has unresolved properties')
value=dt.get(device.path,key)
digest.update(key.encode()+b'\0'+struct.pack('<I',len(value))+value)
if (digest.hexdigest()!=expected or device.sid_count!=streams or
device.windows!=windows or
(policy==DART_SEP_BOOT_V1 and device.device_id!=20) or
(policy==DART_ANE_INIT_V1 and device.device_id!=0) or
(policy==DART_ADRENO_BOOT_V1 and device.device_id!=31)):
raise ValueError(f'{label} DART boot policy differs from the audited topology')
if policy==DART_ANE_INIT_V1:
# The immutable fingerprint includes the exact four APF slices. The
# exception list is present only for slices above relocated DRAM;
# bind its complete value to this tree's placement, not a fixture PA.
dram=dt.nodes.get('/device-tree/chosen',{}).get('dram-base')
indices=[]
if dram is not None:
if dram.placeholder or len(dram.value)!=8:
raise ValueError('ANE DART requires resolved DRAM placement')
base=struct.unpack('<Q',dram.value)[0]
if not 0<base<1<<36:
raise ValueError('ANE DART has invalid DRAM placement')
raw=dt.get(device.path,'dapf-instance-0')
indices=[i for i in range(len(raw)//52)
if struct.unpack_from('<Q',raw,i*52+8)[0]>=base]
expected_allow=struct.pack(f'<{len(indices)}I',*indices) if indices else None
allow=dt.nodes[device.path].get('allow-dram-apf-slices-0')
if ((allow is not None and allow.placeholder) or
dt.get(device.path,'allow-dram-apf-slices-0')!=expected_allow):
raise ValueError('ANE DART APF exceptions differ from DRAM placement')
return policy
@dataclass(frozen=True)
class DeviceModel:
node: str
kind: int
base: int
span: int
config: tuple[int, ...]
def words(self):
return (self.kind, self.base, self.span, *self.config)
@dataclass(frozen=True)
class MCCBootSpec:
node: str
compatible: bytes
amcc_count: int
dcs_count: int
socni_counters: tuple[int, ...]
@dataclass(frozen=True)
class AOPTimebaseBootSpec:
node: str
compatible: bytes
reg_index: int
frequency: int
@dataclass(frozen=True)
class QSPIBootSpec:
node: str
@dataclass(frozen=True)
class DWIStartupSpec:
node: str
@dataclass(frozen=True)
class I2CEmptySpec:
node: str
sha256: str
@dataclass(frozen=True)
class SEPASCSpec:
node: str
sha256: str
reply_transport: bool = False
rom_status: bool = False
service_bootstrap: bool = False
pnon_recovery: bool = False
xars_refusal: bool = False
pnon_refusal: bool = False
sks_startup: bool = False
def sep_asc_models(dt, spec, windows):
"""Exact SEP ASC transport binding; firmware services are separate."""
if type(spec.reply_transport) is not bool:
raise ValueError('SEP ASC transport selection must be explicit')
if type(spec.rom_status) is not bool or (spec.rom_status and not spec.reply_transport):
raise ValueError('SEP ROM status producer requires explicit reply transport')
if type(spec.service_bootstrap) is not bool or (spec.service_bootstrap and not spec.rom_status):
raise ValueError('SEP service bootstrap requires explicit ROM frontend')
if type(spec.pnon_recovery) is not bool or (spec.pnon_recovery and not spec.service_bootstrap):
raise ValueError('SEP ordinary-recovery pnon requires explicit service bootstrap')
if type(spec.xars_refusal) is not bool or (spec.xars_refusal and not spec.service_bootstrap):
raise ValueError('SEP xars refusal service requires explicit service bootstrap')
if type(spec.pnon_refusal) is not bool or (spec.pnon_refusal and not spec.service_bootstrap):
raise ValueError('SEP pnon refusal service requires explicit service bootstrap')
if type(spec.sks_startup) is not bool or (spec.sks_startup and not (
spec.service_bootstrap and spec.xars_refusal and spec.pnon_refusal)):
raise ValueError('SEP SKS startup requires installed pnon/xars services')
if spec.pnon_recovery and spec.pnon_refusal:
raise ValueError('SEP pnon recovery and refusal modes are mutually exclusive')
if (spec.node!='/device-tree/arm-io/sep' or spec.node not in dt.nodes or
tuple(windows)!=((0x282600000,0x88000),(0x282050000,0x60000))):
raise ValueError('SEP ASC register topology changed')
nodes=[n for n in dt.nodes if n==spec.node or n.startswith(spec.node+'/')]
if any(p.placeholder for n in nodes for p in dt.nodes[n].values()):
raise ValueError('SEP ASC has unresolved metadata')
if hashlib.sha256(dt.serialize(spec.node)).hexdigest()!=spec.sha256:
raise ValueError('SEP ASC firmware subtree changed')
if (dt.get(spec.node,'compatible')!=b'iop-sep,ascwrap-v6\0' or
dt.get(spec.node,'interrupts')!=struct.pack('<4I',344,343,346,345) or
dt.get(spec.node,'clock-gates')!=b'' or dt.get(spec.node,'power-gates')!=b''):
raise ValueError('SEP ASC frontend policy changed')
# Preserve every established model index. The frontend covers the
# mailbox pages only and never makes the firmware/security windows RAM.
config=(8,1,346,343,int(spec.rom_status)|int(spec.service_bootstrap)<<1|int(spec.pnon_recovery)<<2|int(spec.xars_refusal)<<3|int(spec.pnon_refusal)<<4|int(spec.sks_startup)<<5) if spec.reply_transport else (8,0,0,0,0)
model=DeviceModel(spec.node,SEP_ASC_MAILBOX_V1,windows[0][0],0x9000,config)
device_model_words((model,))
return (model,)
@dataclass(frozen=True)
class ANSBootSpec:
"""The stock /arm-io/ans and /arm-io/sart-ans nodes, pinned, and the RAM disk -- or, with
window set (full-install M6), the M4 partition on the SSD: disk_bytes is the partition's size
and disk_sha256 is empty (probes/answindow.py)."""
node: str
sha256: str
sart_node: str
sart_sha256: str
disk_sha256: str
disk_bytes: int
window: bool = False
# Window mode: the M4 partition's unique GUID (q1n1 finds it by type and this GUID), and the
# disk, ISC and container GUIDs the synthesized GPT carries (configured.img's, M1 manifest).
window_guid: str = ''
window_gpt: tuple = ()
# /arm-io/ans reg, translated through /arm-io ranges (child 0 -> 0x210000000).
# Indices 2, 7 and 8 are empty (0, 0) and have no window.
ANS_REG_WINDOWS = ((0x389600000,0x88000),(0x389050000,0x4000),(0x38dcc0000,0x60000),
(0x38b000000,0x1000000),(0x38db90000,0xc000),(0x38dd47c00,0x4000),
(0x3cdcc0000,0x10000),(0x38dc00000,0x4000),(0x38dc20000,0x4000),
(0x38db00000,0x20000))
ANS_SART_WINDOWS = ((0x38dc50000,0xc000),(0x38dd44000,0x4000),(0x38dcc0000,0x4000))
ANS_WINDOW_MODE = 1 # compat/ans.h: NVMe model config[3]
ANS_WINDOW_BYTES = 0x500000 # state, firmware, GPT, SSD params/DMA and1MiB staging
def ans_reservation_span(disk_bytes, window=False):
"""Bytes the monitor needs for one virtual ANS2: state, reserved page, disk (or the window's)."""
if window:
if type(disk_bytes) is not int or disk_bytes < 1 << 30 or disk_bytes % (1 << 20):
raise ValueError('invalid virtual ANS2 window size')
return ANS_WINDOW_BYTES
if type(disk_bytes) is not int or not 0 < disk_bytes <= ANS_DISK_MAX or disk_bytes % 0x1000:
raise ValueError('invalid virtual ANS2 disk size')
return (ANS_DISK_OFFSET + disk_bytes + 0xffff) & ~0xffff
# The firmware state iBoot publishes on the IOP nub once it has booted ANS2
# and left it quiesced. The 26A428 template (DeviceTree.j700ap) has none of it;
# the live Mac17,5 ADT does (uuid, segment-names "__TEXT;__DATA",
# segment-ranges, pre-loaded 1, running 1; docs/ANS2-FIRMWARE-STATE-20260925.md).
# Without it RTBuddy::_initConfigEDT (0xfffffe000b327c98) leaves pre-loaded,
# running and no-firmware-service clear, _attemptFirmwareLoad
# (0xfffffe000b320c70) waits for an RTBuddyFirmwareService with Role ANS2 that
# no 26A428 kext provides, and _setPowerStateGuardEntryGated
# (0xfffffe000b322ea0) refuses every power change without firmware: the IOP is
# never woken. With it, _handlePreloadFirmware -> loadFirmware -> setPowerState(1).
ANS_NUB = '/iop-ans-nub'
# segment-ranges flags word (RTBuddy::getSegmentMap 0xfffffe000b328a88):
# bit0 read-only, bit2 needs backup, bit3 AP-kernel memory, bit4 restricted
# (RTBuddyCoredumpMap::initWithSegments 0xfffffe000b313a84 never maps it).
ANS_SEGMENT_READ_ONLY = 0x1
ANS_SEGMENT_RESTRICTED = 0x10
ANS_FIRMWARE_PROPERTIES = ('segment-names', 'segment-ranges', 'pre-loaded', 'running')
def ans_firmware_state(reservation):
"""The nub properties that describe the virtual IOP as iBoot-booted and quiesced.
One segment, "__TEXT", at IOP address 0: the monitor's reserved zero page
(reservation + ANS_FW_OFFSET), read-only and restricted, so XNU records it
in a physical memory descriptor and never maps it. No firmware image is
presented and nothing the guest reaches is backed by it; an access would
stop the run at stage 2 with this address. Record layout (32 bytes):
physical address, IOP address, physical address, u32 size, u32 flags."""
if type(reservation) is not int or reservation<=0 or reservation%0x10000:
raise ValueError('invalid virtual ANS2 reservation')
pa=reservation+ANS_FW_OFFSET
return (('segment-names', b'__TEXT\0'),
('segment-ranges', struct.pack('<QQQII', pa, 0, pa, ANS_FW_BYTES,
ANS_SEGMENT_READ_ONLY|ANS_SEGMENT_RESTRICTED)),
('pre-loaded', struct.pack('<I', 1)),
('running', struct.pack('<I', 1)))
def _ans_stock_subtree(dt, node):
"""The ANS2 subtree serialized without the published firmware state."""
props=dt.nodes.get(node+ANS_NUB)
if props is None:
raise ValueError('ANS2 IOP nub missing')
saved=dict(props)
try:
for name in ANS_FIRMWARE_PROPERTIES:
props.pop(name, None)
return dt.serialize(node)
finally:
props.clear()
props.update(saved)
def populate_ans_firmware(dt, spec, reservation):
"""Publish the virtual ANS2's firmware state on the stock, pinned nub.
Only these four properties are added; the template must carry none of
them and must still be the audited subtree (quiesced, power-managed,
no user power management, no firmware-service opt-out, empty region)."""
nub=spec.node+ANS_NUB
props=dt.nodes.get(nub)
if spec.node!='/device-tree/arm-io/ans' or props is None:
raise ValueError('ANS2 node identity changed')
if any(name in props for name in ANS_FIRMWARE_PROPERTIES):
raise ValueError('ANS2 IOP nub already carries firmware state')
if hashlib.sha256(dt.serialize(spec.node)).hexdigest()!=spec.sha256:
raise ValueError(f'ANS2 firmware subtree changed: {spec.node}')
if (dt.get(nub,'compatible')!=b'iop-nub,rtbuddy-v2\0' or dt.get(nub,'quiesced')!=b'' or
dt.get(nub,'power-managed')!=struct.pack('<I',1) or
any(dt.get(nub,k) is not None for k in ('user-power-managed','no-firmware-service',
'dont-power-on','reconfig-firmware','uuid')) or
dt.get(nub,'region-base')!=bytes(8) or dt.get(nub,'region-size')!=bytes(8)):
raise ValueError('ANS2 IOP nub contract changed')
for name,value in ans_firmware_state(reservation):
dt.add(nub, name, value)
def ans_models(dt, spec, windows, sart_windows, reservation):
"""Bind the stock ANS2 nodes to the monitor's virtual ANS2 (compat/ans.c).
Apple's own AppleASCWrapV6/RTBuddy/IONVMeFamily drivers match these nodes
unchanged; only the register windows the 26A428 drivers touch outside the
SPTM are modeled (the ASC mailbox pages and the NVMe BAR, which holds the
NVMMU and the coastguard SART window). Everything else stays absent at
stage 2, so an unexpected access stops the run with its IPA. The nub must
carry exactly the firmware state populate_ans_firmware publishes for this
reservation, after the pinned template properties.
"""
if (spec.node!='/device-tree/arm-io/ans' or spec.sart_node!='/device-tree/arm-io/sart-ans' or
spec.node not in dt.nodes or spec.sart_node not in dt.nodes):
raise ValueError('ANS2 node identity changed')
if tuple(windows)!=ANS_REG_WINDOWS or tuple(sart_windows)!=ANS_SART_WINDOWS:
raise ValueError('ANS2 register topology changed')
for node,digest in ((spec.node,spec.sha256),(spec.sart_node,spec.sart_sha256)):
nodes=[n for n in dt.nodes if n==node or n.startswith(node+'/')]
if any(p.placeholder for n in nodes for p in dt.nodes[n].values()):
raise ValueError('ANS2 has unresolved metadata')
serialized=_ans_stock_subtree(dt, node) if node==spec.node else dt.serialize(node)
if hashlib.sha256(serialized).hexdigest()!=digest:
raise ValueError(f'ANS2 firmware subtree changed: {node}')
nub=dict(dt.nodes[spec.node+ANS_NUB])
if (list(nub)[-len(ANS_FIRMWARE_PROPERTIES):]!=list(ANS_FIRMWARE_PROPERTIES) or
tuple((k,bytes(nub[k].value)) for k in ANS_FIRMWARE_PROPERTIES)!=ans_firmware_state(reservation)):
raise ValueError('ANS2 IOP firmware state differs from the virtual ANS2')
if (dt.get(spec.node,'compatible')!=b'iop,ascwrap-v6\0' or dt.get(spec.node,'role')!=b'ANS2\0' or
dt.get(spec.node,'interrupts')!=struct.pack('<5I',1008,1007,1010,1009,1039) or
dt.get(spec.node,'nvme-interrupt-idx')!=struct.pack('<I',4) or
dt.get(spec.node,'nvme-queue-entries')!=struct.pack('<I',64) or
dt.get(spec.node,'nvme-linear-sq')!=b'' or
dt.get(spec.node,'iommu-parent')!=dt.get(spec.sart_node,'AAPL,phandle') or
dt.get(spec.sart_node,'compatible')!=b'sart,coastguard\0' or
dt.get(spec.sart_node,'sart-version')!=struct.pack('<I',3)):
raise ValueError('ANS2 driver contract changed')
pinned = (len(spec.window_guid) == 36 and len(spec.window_gpt) == 3 if spec.window else
type(spec.disk_sha256) is str and len(spec.disk_sha256) == 64) # window: the partition GUID pins it
if (not pinned or type(reservation) is not int or reservation<=0 or reservation%0x10000):
raise ValueError('invalid virtual ANS2 reservation')
ans_reservation_span(spec.disk_bytes, spec.window)
if spec.window and reservation % (1 << 20):
raise ValueError('the window reservation must be 1 MiB aligned (the SSD DMA window)')
models=(DeviceModel(spec.node,ANS_ASC_V1,*ANS_ASC_WINDOW,(reservation,1010,1007,0,0)),
DeviceModel(spec.node,ANS_NVME_V1,*ANS_NVME_WINDOW,
(reservation,spec.disk_bytes,1039,ANS_WINDOW_MODE if spec.window else 0,0)))
device_model_words(models)
return models
def i2c_empty_models(dt, spec, windows):
"""An exact I2C2 frontend with no slave endpoints, hence address NACKs."""
if (spec.node != '/device-tree/arm-io/i2c2' or
tuple(windows) != ((0x385018000, 0x4000),) or
spec.node not in dt.nodes):
raise ValueError('I2C bus differs from the audited topology')
nodes=[n for n in dt.nodes if n==spec.node or n.startswith(spec.node+'/')]
if any(p.placeholder for n in nodes for p in dt.nodes[n].values()):
raise ValueError('I2C bus contains unresolved metadata')
if hashlib.sha256(dt.serialize(spec.node)).hexdigest()!=spec.sha256:
raise ValueError('I2C bus firmware changed')
expected={
'clock-gates': struct.pack('<I',69),
'interrupts': struct.pack('<I',1110),
'interrupt-parent': struct.pack('<I',69),
}
if any(dt.get(spec.node,k)!=v for k,v in expected.items()):
raise ValueError('I2C clock or interrupt contract changed')
prop=dt.nodes.get('/device-tree/arm-io/pmgr',{}).get('devices')
row=bytes.fromhex('00000000360000000000000000000000d802000000000000'
'000045000000000049324332000000000000000000000000')
if prop is None or prop.placeholder or prop.value[3264:3312]!=row:
raise ValueError('I2C2 PMGR gate changed')
# Revision 6 describes our implemented master enable/control frontend,
# not a sampled Qualcomm or physical Apple controller identity.
model=DeviceModel(spec.node,I2C_EMPTY_V1,windows[0][0],0x4000,(2,6,1110,0,0))
device_model_words((model,))
return (model,)
def dwi_models(dt, spec, windows):
"""Bind the stock DWI startup programming; transactions remain trapped."""
expected = {
'dwi-version': struct.pack('<I', 1), 'lockout-us': struct.pack('<I', 5),
'compatible': b'dwi,t8101\0dwi,s8000\0',
'interrupt-parent': struct.pack('<I', 69), 'interrupts': struct.pack('<I', 23),
'clock-gates': struct.pack('<I', 13),
'reg': struct.pack('<2Q', 0xf0200000, 0x4000),
'AAPL,phandle': struct.pack('<I', 73), 'str-delay': struct.pack('<I', 8000),
'polarity-config': struct.pack('<I', 1), 'device_type': b'dwi\0',
'nclk-div': struct.pack('<I', 1), 'name': b'dwi\0',
}
if (spec.node != '/device-tree/arm-io/dwi' or
tuple(windows) != ((0x300200000, 0x4000),) or
set(dt.nodes.get(spec.node, {})) != set(expected)):
raise ValueError('DWI controller differs from the audited topology')
for name, value in expected.items():
prop = dt.nodes[spec.node][name]
if prop.placeholder or prop.value != value:
raise ValueError(f'DWI controller differs at {name}')
# Gate 13 is a fixed-active root at PMGR group0+118, not a guessed clock.
prop = dt.nodes.get('/device-tree/arm-io/pmgr', {}).get('devices')
row = bytes.fromhex('010000000000000000000000000000001801000000000000'
'00000d000000000044574900000000000000000000000000')
if prop is None or prop.placeholder or prop.value[576:624] != row:
raise ValueError('DWI PMGR gate differs from the audited topology')
model = DeviceModel(spec.node, DWI_STARTUP_V1, windows[0][0], 0x4000, (0, 0, 0, 0, 0))
device_model_words((model,))
return (model,)
def qspi_models(dt, spec, windows):
"""Bind the audited overlapping QSPIMC windows to one virtual controller.
Reset/identity only initially; a flash command is not acknowledged without
an actual transaction backend. Keep the real flash/NVRAM consumers present.
"""
expected = {
'compatible': b'qspi,qspimc\0', 'clock-ids': struct.pack('<I', 396),
'interrupt-parent': struct.pack('<I', 69), 'interrupts': struct.pack('<I', 1106),
'reg': struct.pack('<4Q', 0x175110000, 0x4000, 0x175110800, 0x4000),
'AAPL,phandle': struct.pack('<I', 156), 'clock-gates': struct.pack('<I', 84),
'device_type': b'qspi\0', '#size-cells': struct.pack('<I', 7),
'#address-cells': struct.pack('<I', 1), 'spi-version': struct.pack('<I', 1),
'name': b'qspi\0',
}
if (spec.node != '/device-tree/arm-io/qspi' or
tuple(windows) != ((0x385110000, 0x4000), (0x385110800, 0x4000)) or
set(dt.nodes.get(spec.node, {})) != set(expected)):
raise ValueError('QSPI controller differs from the audited topology')
for name, value in expected.items():
prop = dt.nodes[spec.node][name]
if prop.placeholder or prop.value != value:
raise ValueError(f'QSPI controller differs at {name}')
model = DeviceModel(spec.node, QSPI_BOOT_V1, windows[0][0], 0x5000, (0, 0, 0, 0, 0))
device_model_words((model,))
return (model,)
def aop_timebase_models(dt, spec, windows, frequency):
"""Free-running AOP counter shared with ADMAC, not an AOP firmware peer."""
nub = spec.node + '/iop-aop-nub'
index = dt.get(nub, 'aop-timesync-reg-index')
if (dt.get(spec.node, 'compatible') != spec.compatible + b'\0' or
dt.get(nub, 'compatible') != b'iop-nub,rtbuddy-v2\0' or
dt.get(nub, 'aop-fr-timebase') != struct.pack('<I', 1) or
index not in (None, struct.pack('<I', spec.reg_index)) or
spec.reg_index != 3 or len(windows) != 5 or windows[3][1] != 8 or
spec.frequency != 24000000 or frequency != spec.frequency):
raise ValueError('AOP free-running timebase differs from the audited ABI')
model = DeviceModel(spec.node, AOP_TIMEBASE_V1, windows[3][0], 0x1000,
(frequency, 0, 0, 0, 0))
device_model_words((model,))
return (model,)
def pmgr_cpu_boot_policy(dt):
"""Bind zero software votes to the actual single-CPU guest handoff.
cpu0 executes; the other five declared CPUs have never been released.
Both cluster-control interfaces remain held active. Power/launch changes
need a future CPU lifecycle backend, rather than fixed-active PS requests.
"""
prefix='/device-tree/cpus/'
nodes={path for path in dt.nodes if path.startswith(prefix) and path.count('/')==3}
if nodes!={prefix+f'cpu{i}' for i in range(6)}:
raise ValueError('CPU power boot policy requires the audited six-node topology')
for cpu in range(6):
cluster=int(cpu>=4);node=prefix+f'cpu{cpu}'
expected={'cpu-id':struct.pack('<I',cpu),'cluster-id':struct.pack('<I',cluster),
'reg':struct.pack('<I',cpu if cpu<4 else 0x100+cpu-4),
'cluster-type':b'P\0' if cluster else b'E\0',
'state':b'waiting\0' if cpu else b'running\0'}
for key,value in expected.items():
prop=dt.nodes[node].get(key)
if prop is None or prop.placeholder or prop.value!=value:
raise ValueError(f'{node}: CPU power boot policy differs at {key}')
return PMGR_CPU0_BOOT_V1
def pmgr_power_models(dt, spec, windows):
"""Hash-bound PS and characterization banks, without host MMIO.
Most system domains remain active. Audited idle media, ATC, SPI, I2C and PWM leaves support
dependency-checked explicit gating; ATC and ANE additionally bind their
DART banks and ANE rejects virtual PMP claims. Automatic idle, reset and access
suppression remain unsupported.
AVD_SYS, ISP_SYS, PRORES and VENC0/1 admit off/on and atomic AP-voter transitions
with DART checks; physical PMP ACK follows committed power. ISP parent
and child levels constrain each other in both directions. VENC systems
and their virtual PMC cluster have bidirectional power dependencies.
MSR owns its ASE child and scaler DART; JPG owns both JPEG DART banks.
GFX transitions require external pristine-UAT admission; graphics engines
and their mapper remain unsupported.
SOC_RC, PMGR_SOC_OCLA and AP_TMM are independent non-PMP idle leaves,
with no execution/sample producers or declared resource consumers.
SEP_AON owns a separate idle lifecycle with exact SEP DART binding and
no virtual SEP vote. Main SEP stays active; firmware/services are unimplemented.
CPU/CPM boot policy separates software requests (zero) from actual state:
cpu0 and the independently held-active clusters are on; five CPUs wait off.
"""
props=dt.nodes.get(spec.node,{})
hashes={
'devices':'695eaa2f3def5f2fe9e7e8a97306b2ba44ff2f246b97c4f80a8c46bd496aa384',
'reg':'228f2f75c199e9c0cdb99d16d484c2dade36d02673f8826d062e195de2b446f3',
'perf-groups':'15b1245f5b613775f5c2b747d1e930414b12dfd49c0cb68875732390c6b702ed',
'perf-counters':'0bee9adb92308ec5cad0869c5eabe2b121db31eeb61d558309221ca3be4cc096',
'perf-domains':'9ae7960cd20eca139de2b38146419d9ad3155af5b9bec98312be9769ca592b33',
'ap-wake-sources':'e989ecf7f4b46a3453704c3563b05f12c668bb7952deca38463bf719ac382e31',
'ap-wake-source-flags':'606cc978178d2af3958ed6d1cb67525104cda0879b94021f9598efe635a40ec8',
'voter-rail-data':'fbecd971bd7e03fe63153bb92e2ba303f28cb929fbbd9b1cd596242222de97dd',
'voter-agent':'aa25ccd1005e5d06a579c23a11fc4c050a331e0bd07cc5c4c1d906a377c44560',
'boost-agent':'3007af003de5a4be42dddd957196e8a3040a39af12c9df583b3a0e493fb2bf98',
'bwr-agent':'edb42c0c466e73e181537b6abdbfaa29a5e9210a3b0a13df1e251b96d2cb138b',
'dvmr-agent':'4ed7013ffaa2d37000565d450866517f6d43c0aa8e9c33c227d2ca4f1316a55d',
'cluster-agent':'e0508b7cc03f2cc12f1fd2715e4d187c717e7a5fd7a15d1fa42d9800624352f1',
'soc-clusters':'809aa1d422c55ef71a584f8165b0a5dd45ca0441577e86f2e0812116c02f803d',
'soc-cluster-power-gating':'dc765660b06ee03dd16fd7ca5b957e8c805161ac2c4af28c5a100ab2ab432ca1',
}
if (dt.get(spec.node,'compatible')!=spec.compatible+b'\0' or
'compatible' not in props or props['compatible'].placeholder or
dt.get(spec.node,'ps-groups')!=struct.pack('<6I',0,0,0,1,0,0) or
props['ps-groups'].placeholder or len(windows)!=44 or
windows[0][1]!=0xcc000 or windows[1][1]!=0x80000 or
windows[30]!=(0x3082c8000,0x4000)):
raise ValueError('PMGR power-state topology differs from the audited ABI')
for name,digest in hashes.items():
if (name not in props or props[name].placeholder or
hashlib.sha256(bytes(props[name].value)).hexdigest()!=digest):
raise ValueError(f'PMGR power-state {name} topology changed')
cpu_policy=pmgr_cpu_boot_policy(dt)
models=tuple(DeviceModel(spec.node,PMGR_ACTIVE_BOOT_V1,windows[g][0],span,(g,0 if g else cpu_policy,0,0,0))
for g,span in enumerate((0x11000,0x9000)))
# Logical PMGR reg 108 maps to DT reg 30 in the pinned T8140 driver.
# Immutable process/leakage and non-silicon identity fields describe only
# the virtual Apple backend, never physical Qualcomm fuse contents.
models+=(DeviceModel(spec.node,PMGR_AON_FUSES_V1,*windows[30],(0,0,0,0,0)),)
# getDVMRMaxWeight uses table index 1 for both single-state CPU domains.
# readACCReg routes 0xe78048 through PMGR logical regs 11/23 (DT 8/17).
# Cluster 1's FANE word must index a backed ANE metadata entry.
if ('voltage-states8' not in props or props['voltage-states8'].placeholder or
dt.get(spec.node,'voltage-states8')!=b''.join(struct.pack('<II',*e) for e in VIRTUAL_ANE_STATES)):
raise ValueError('PMGR FANE requires backed inactive and active virtual ANE states')
for cluster,domain,index,base in ((0,1,8,0x210e50000),(1,5,17,0x211e50000)):
name=f'voltage-states{domain}'
if (windows[index]!=(base,0xe81000) or name not in props or
props[name].placeholder or dt.get(spec.node,name)!=struct.pack('<II',65536,0)):
raise ValueError('PMGR DVMR single-state CPU contract changed')
models+=(DeviceModel(spec.node,PMGR_CPU_PERF_BOOT_V1,base+0x28000,0x1000,(cluster,0,0,0,0)),)
for cluster,index,base in ((0,9,0x210e70000),(1,18,0x211e70000)):
if windows[index]!=(base,0x1000):
raise ValueError('PMGR DVMR weight-table window changed')
models+=(DeviceModel(spec.node,PMGR_DVMR_WEIGHTS_V1,base,0x1000,(cluster,0,0,0,0)),)
# AON perf-groups 6/7. Hash-bound tables include paired type-1 controls;
# only their configuration words are implemented, not sampled telemetry.
for group,index,window,offset in ((6,3,(0x308200000,0x7c000),0x7b000),
(7,1,(0x308280000,0x80000),0x34000)):
if windows[index]!=window:
raise ValueError('PMGR AON counter configuration window changed')
models+=(DeviceModel(spec.node,PMGR_AON_COUNTER_CONFIG_V1,
window[0]+offset,0x1000,(group,0,0,0,0)),)
if windows[4]!=(0x308004000,0x1000):
raise ValueError('PMGR AP wake-source mask window changed')
models+=(DeviceModel(spec.node,PMGR_AP_WAKE_MASK_V1,*windows[4],(0,0,0,0,0)),)
# ApplePTD::_setIrq/_enableIrq/_disableIrq, format 1. The data and
# update aliases remain owned by the separate MCC transport descriptor.
pmp=dt.nodes.get('/device-tree/arm-io/pmp/iop-pmp-nub',{})
for node,prop,digest in ((props,'ptd-ranges',
'badb95d4143a7953cf071ff545271711fd754331253e5d2b966a4f47459ac69e'),
(pmp,'ptd-range','2114f1290101b1d573fcae3ac905ccc322f30ee1e125fa54511bd1e825bcc29a')):
if (prop not in node or node[prop].placeholder or
hashlib.sha256(bytes(node[prop].value)).hexdigest()!=digest):
raise ValueError('PTD interrupt directory differs from the audited ABI')
version=pmp.get('ptd-driver-version')
if (version is None or version.placeholder or version.value!=struct.pack('<I',1) or
windows[34]!=(0x3003c0000,0x30000)):
raise ValueError('PTD interrupt format or aperture changed')
models+=(DeviceModel(spec.node,PTD_IRQ_CONFIG_V1,windows[34][0]+0x18000,
0x1000,(0,0,0,0,0)),)
# Logical reg 98 resolves through DT 37; DT 28 aliases the same bank.
# Logical reg 101 is DT 22. Own each page once, preserving strict holes.
if (windows[28]!=(0x300780000,0x4000) or windows[37]!=windows[28] or
windows[25]!=(0x300760000,0x4000) or
windows[22]!=(0x300724000,0x4000)):
raise ValueError('PMGR CPU cluster/gate windows changed')
models+=(DeviceModel(spec.node,PMGR_CPU_CLUSTER_ACTIVE_V1,windows[37][0],
0x1000,(PMGR_AVE_CLUSTER_V1,0,0,0,0)),
DeviceModel(spec.node,PMGR_SOC_GATE_POLICY_V1,windows[22][0],
0x1000,(0,0,0,0,0)))
# restoreSOCDPE binds logical reg 78 to DT 32. Its 29 configuration
# words occupy only three pages of the source 32 KiB mapping.
if windows[32]!=(0x300208000,0x8000):
raise ValueError('PMGR SOC DPE configuration window changed')
models+=(DeviceModel(spec.node,PMGR_SOC_DPE_POLICY_V1,
windows[32][0]+0x2000,0x3000,(0,0,0,0,0)),)
workaround=props.get('axi2af-hack')
if (workaround is None or workaround.placeholder or
workaround.value!=struct.pack('<I',0x3ff)):
raise ValueError('PMGR AXI2AF workaround domain mask changed')
physical_records=dt.get(spec.node,'devices')
physical_ps={struct.unpack_from('<I',physical_records,i+16)[0]
for i in range(0,len(physical_records),48) if not physical_records[i]&16}
if any(offset not in physical_ps for offset,_,_ in AXI2AF_BANKS_V1):
raise ValueError('PMGR AXI2AF domain lacks physical PS backing')
models+=tuple(DeviceModel(spec.node,PMGR_AXI2AF_POLICY_V1,base,0x1000,(0,0,0,0,0))
for _,base,_ in AXI2AF_BANKS_V1)
# PMS_PMC is logical reg 129 / DT 43. The captured wrapper adds 0x800000
# to T8140 PMC offsets; read/writeEnableRegister then adds 0xac000.
if windows[43]!=(0x302000000,0x1000000):
raise ValueError('PMC enable register aperture changed')
for name,value in (('pmc',1),('pmc-pmgr',0x3f),('pmc-ltr-ka',1)):
prop=props.get(name)
if prop is None or prop.placeholder or prop.value!=struct.pack('<I',value):
raise ValueError('PMC enable policy differs from the audited configuration')
models+=(DeviceModel(spec.node,PMC_ENABLE_POLICY_V1,windows[43][0]+0x8ac000,
0x1000,(0,0,0,0,0)),
DeviceModel(spec.node,PMC_RMBS_REQUEST_V1,windows[43][0]+0x860000,
0x1000,(0,0,0,0,0)),
DeviceModel(spec.node,PMC_DVMR_CONFIG_V1,windows[43][0]+0x8a4000,
0x6000,(0,0,0,0,0)))
# Hash-bound soc-clusters row 2 is AVE, cluster 3 / TVM 11. This
# descriptor owns only disable/idle for the unsampled virtual TVM.
for unit,suffix in ((0,''),(1,'1')):
step=unit*0x4000000
for name,pairs in (
('ave',((0x75100000+step,0x45c000),(0x75800000+step,0x800000),
(0x75050000+step,0x4000),(0xf0708000+unit*0x28,0x4000),
(0x74000000+step,0x1000000))),
('dart-ave',((0x75080000+step,0xc000),(0x750a0000+step,0xc000),
(0x75094000+step,0x4000),(0x750b0000+step,0x4000)))):
prop=dt.nodes.get('/device-tree/arm-io/'+name+suffix,{}).get('reg')
expected=b''.join(struct.pack('<QQ',*pair) for pair in pairs)
if prop is None or prop.placeholder or bytes(prop.value)!=expected:
raise ValueError('AVE execution/APF resource windows changed')
models+=(DeviceModel(spec.node,PMGR_AVE_TVM_IDLE_V1,windows[25][0],
0x1000,(0,0,0,0,0)),)
# _restorePerfCountersV2 restores non-AON groups 0..5. The native group
# table normalizes DT indices 2/3/43 to logical registers 91/93/129;
# captured page-table walks bind these to the original DT apertures.
groups=dt.get(spec.node,'perf-groups')
for group,index,window,off,span in (
(0,2,(0x300000000,0x78000),0x6d800,0x2000),
(1,0,(0x300700000,0xcc000),0x20000,0x3000),
(2,0,(0x300700000,0xcc000),0x78000,0x1000),
(3,0,(0x300700000,0xcc000),0x7c000,0x1000),
(4,0,(0x300700000,0xcc000),0xa0000,0x1000),
(5,43,(0x302000000,0x1000000),0x900000,0x1000)):
if windows[index]!=window or groups[group*36:(group+1)*36]!=struct.pack(
'<9I',index,off,1,0x10,3,4,0,0,0):
raise ValueError('SOC counter restore group geometry changed')
models+=(DeviceModel(spec.node,PMGR_SOC_PERF_RESTORE_V1,
(window[0]+off)&~0xfff,span,(group,0,0,0,0)),)
for cluster,index,base in ((0,5,0x210e20000),(1,14,0x211e20000)):
if windows[index]!=(base,0x2000):
raise ValueError('ACC counter restore window changed')
models+=(DeviceModel(spec.node,PMGR_ACC_PERF_RESTORE_V1,
base,0x2000,(cluster,0,0,0,0)),)
# Stock core/CPM energy setters address a pair of 64-bit baselines.
# Keep each exact page separate from unrelated implementation registers.
for index,base,span,offset in (
(10,0x210050000,0x10000,0x9000),(11,0x210150000,0x10000,0x9000),
(12,0x210250000,0x10000,0x9000),(13,0x210350000,0x10000,0x9000),
(19,0x211050000,0x10000,0x9000),(20,0x211150000,0x10000,0x9000),
(7,0x210e40000,0x9000,0x8000),(16,0x211e40000,0xb000,0x8000)):
if windows[index]!=(base,span):
raise ValueError('CPU energy restore window changed')
models+=(DeviceModel(spec.node,PMGR_ENERGY_RESTORE_V1,
base+offset,0x1000,(0,0,0,0,0)),)
for cluster,index,base,span in ((0,7,0x210e40000,0x9000),
(1,16,0x211e40000,0xb000)):
if windows[index]!=(base,span):
raise ValueError('ACC startup policy window changed')
models+=(DeviceModel(spec.node,PMGR_ACC_BOOT_POLICY_V1,
base+0x4000,0x1000,(cluster,0,0,0,0)),)
# enableLltLimit uses logical SOC-misc register 105, DT window 24,
# at +0x10000. Only its disabled setting has a virtual backend.
if windows[24]!=(0x300730000,0x14000):
raise ValueError('SOC misc LLT limit window changed')
models+=(DeviceModel(spec.node,PMGR_LLT_DISABLED_V1,
windows[24][0]+0x10000,0x1000,(0,0,0,0,0)),)
device_model_words(models)
return models
def dart_models(dt, devices):
"""Bind DART register instances; APF/SMMU windows keep separate contracts.
Instance records are 16-byte type/index/name/variant tuples. Their order
is the reg-array order; index names the logical instance, not a reg slot.
The two alternative JPEG nodes share one physical DART. Admit that one
audited alias explicitly, with capacity covering either profile.
"""
models=[]
by_base={}
jpeg={'/device-tree/arm-io/dart-jpeg-h17a','/device-tree/arm-io/dart-jpeg0-h17p'}
for device in devices:
raw=dt.get(device.path,'instance') or b''
irq=dt.get(device.path,'interrupts') or b''
if (device.compatible!=b'dart,t8110' or device.page_size!=0x4000 or
not raw or len(raw)!=16*len(device.windows) or len(irq)!=4 or
dt.nodes[device.path]['instance'].placeholder or
dt.nodes[device.path]['interrupts'].placeholder):
raise ValueError(f'{device.path}: unaudited DART instance geometry')
if not 0<=device.device_id<32:
raise ValueError('DART model initialization ID exceeds the admitted bitmap')
boot_policy=_dart_boot_policy(dt,device)
# The pinned INIT handler walks the DART records, excluding ANE's
# DAPF record. Admit that exact mixed topology for capability capture
# only; APF setup, POWERUP and frame ownership remain separate.
pure=all(raw[i:i+4]==b'TRAD' for i in range(0,len(raw),16))
init_ids=(1<<device.device_id) if pure or boot_policy==DART_ANE_INIT_V1 else 0
for n,(base,span) in enumerate(device.windows):
kind=raw[16*n:16*n+4]
if kind in (b'UMMS',b'FPAD'):continue
if kind!=b'TRAD':raise ValueError(f'{device.path}: unknown IOMMU instance type')
model=DeviceModel(device.path,DART_T8110_V1,base,span,
(device.sid_count,boot_policy,struct.unpack('<I',irq)[0],36,init_ids))
if base in by_base:
old=models[by_base[base]]
if ({old.node,device.path}!=jpeg or span!=old.span or
model.config[1:4]!=old.config[1:4]):
raise ValueError('unrecognized DART register alias')
models[by_base[base]]=replace(old,config=(max(old.config[0],model.config[0]),
*old.config[1:4],old.config[4]|model.config[4]))
else:
by_base[base]=len(models)
models.append(model)
device_model_words(models)
return tuple(models)
@dataclass(frozen=True)
class MCCPTDBootSpec:
node: str
compatible: bytes
pmp_node: str
quota_words: int
@dataclass(frozen=True)
class AICBootSpec:
node: str
compatible: bytes
irq_count: int
event_offset: int
revision: int
@dataclass(frozen=True)
class WatchdogBootSpec:
node: str
compatible: bytes
version: int
clock_id: int
frequency: int
@dataclass(frozen=True)
class GPIOIdleRX:
peripheral: str
pin: int
mux: int
@dataclass(frozen=True)
class GPIOIdleCLKREQ:
bridge: str
pin: int
mux: int
@dataclass(frozen=True)
class GPIOIdleTX:
peripheral: str
pin: int
mux: int
@dataclass(frozen=True)
class GPIOIdleI2C:
peripheral: str
scl: int
sda: int
mux: int
@dataclass(frozen=True)
class GPIOBootSpec:
node: str
pins: int
groups: int
supported: tuple[int, ...]
idle_rx: tuple[GPIOIdleRX, ...] = ()
idle_clkreq: tuple[GPIOIdleCLKREQ, ...] = ()
idle_tx: tuple[GPIOIdleTX, ...] = ()
idle_i2c: tuple[GPIOIdleI2C, ...] = ()
@dataclass(frozen=True)
class AESKeysBootSpec:
node: str
register_index: int
offsets: tuple[int, ...]
engine_version: int | None = None
software_dma: bool = False
@dataclass(frozen=True)
class SPMIBootSpec:
node: str
aic_node: str
generation: int = 3
queue_depth: int = 256
rtc: bool = False
@dataclass(frozen=True)
class DockBootSpec:
node: str
mask: int
@dataclass(frozen=True)
class DockFIFOBootSpec:
node: str
aic_node: str
def dock_fifo_models(dt, spec, windows):
expected = ((0, 0x1000), (0x14000, 0x1000), (0x30000, 0x1000),
(0x34000, 0x1000), (0x28000, 0x1000), (0x2c000, 0x1000))
if (dt.get(spec.node, 'compatible') != b'dockchannel,t8002\0' or
dt.get(spec.node, 'channel-number') != struct.pack('<I', 0) or
dt.get(spec.node, 'fifo-size') != struct.pack('<I', 2048) or
dt.get(spec.node, 'dock-wstat-mask') != struct.pack('<I', 0xfff) or
len(windows) != 6 or
tuple((pa-windows[0][0], size) for pa, size in windows) != expected):
raise ValueError('DockChannel node does not match the audited single-channel FIFO ABI')
aic = dt.get(spec.aic_node, 'AAPL,phandle')
interrupts = dt.get(spec.node, 'interrupts')
if (len(aic) != 4 or not int.from_bytes(aic, 'little') or
dt.get(spec.node, 'interrupt-parent') != aic or len(interrupts) != 4):
raise ValueError('DockChannel requires one direct AIC interrupt')
irq, = struct.unpack('<I', interrupts)
models = (DeviceModel(spec.node, DOCK_FIFO_V1, windows[0][0], 0x35000,
(0x14000, 0x28000, irq, 2048, 0)),)
device_model_words(models)
return models
def dock_models(dt, spec):
# This audited root node uses absolute u64 address/size, unlike arm-io
# children whose register addresses must pass through the bus ranges.
if (spec.node.rsplit('/', 1)[0] != '/device-tree' or
dt.get(spec.node, 'compatible') != b'dock,connect,aic\0' or
dt.get(spec.node, 'dock-connect-mask') != struct.pack('<Q', spec.mask)):
raise ValueError('dock node does not match the audited status ABI')
reg = dt.get(spec.node, 'reg')
if len(reg) != 16:
raise ValueError('dock status must have one absolute register')
pa, size = struct.unpack('<2Q', reg)
if size != 4 or pa & 3:
raise ValueError('dock status must be one aligned word')
models = (DeviceModel(spec.node, DOCK_ABSENT_V1, pa & ~0xfff, 0x1000,
(pa & 0xfff, spec.mask, 0, 0, 0)),)
device_model_words(models)
return models
def spmi_models(dt, spec, windows):
if type(spec.rtc) is not bool:
raise ValueError('RTC admission must be boolean')
if (dt.get(spec.node, 'compatible').rstrip(b'\0') != b'aapl,spmi' or
spec.generation != 3 or spec.queue_depth != 256 or
dt.get(spec.node, 'gen') != struct.pack('<I', spec.generation) or
dt.get(spec.node, 'queue-depth') != struct.pack('<2I', spec.queue_depth, spec.queue_depth) or
len(windows) != 3 or any(size != 0x4000 for _, size in windows)):
raise ValueError('SPMI node does not match the audited gen-3 FIFO ABI')
# The provider's second interrupt goes to AIC; its first and third are
# internal sources in this SPMI interrupt controller's own namespace.
own = dt.get(spec.node, 'AAPL,phandle')
aic = dt.get(spec.aic_node, 'AAPL,phandle')
interrupts = dt.get(spec.node, 'interrupts')
if (len(own) != 4 or len(aic) != 4 or own == aic or
dt.get(spec.node, 'interrupt-parent') != own+aic+own or
len(interrupts) < 12 or len(interrupts) % 4 or
struct.unpack_from('<I', interrupts)[0] != 256):
raise ValueError('unsupported SPMI parent interrupt routing')
irq = struct.unpack_from('<I', interrupts, 4)[0]
base = windows[2][0]
model = DeviceModel(spec.node, SPMI_GEN3_EMPTY_V1, base,
windows[0][0]+0x4000-base,
(windows[0][0]-base, windows[1][0]-base,
irq, spec.queue_depth, spec.queue_depth))
if spec.rtc:
pmu='/device-tree/arm-io/nub-spmi0/pmu-main'
if (spec.node!='/device-tree/arm-io/nub-spmi0' or base!=0x308700000 or irq!=487 or
pmu not in dt.nodes or hashlib.sha256(dt.serialize(pmu)).hexdigest()!=
'a898f44f3563763664c55b0edad99c46007993978c18391ba7a91d74355e8ff7'):
raise ValueError('RTC PMU subtree differs from the audited J700 contract')
# Zero is an unseeded descriptor, admitted for construction but refused
# by device_model_start. The launcher supplies host UTC just before entry.
model=replace(model,kind=SPMI_PMU_RTC_V1,config=model.config[:3]+(0,PMU_RTC_CONTRACT))
device_model_words((model,))
return (model,)
def seed_rtc_models(models, epoch_us):
"""Seed the single explicitly admitted RTC; never rewrite a previous seed."""
if type(epoch_us) is not int or not 0<epoch_us<=PMU_RTC_LIMIT_US:
raise ValueError('RTC UTC seed is outside the 47-bit counter range')
indices=[i for i,m in enumerate(models) if m.kind==SPMI_PMU_RTC_V1]
if len(indices)!=1 or models[indices[0]].config[3]!=0:
raise ValueError('RTC seeding requires exactly one unseeded clock')
result=tuple(replace(m,config=m.config[:3]+(epoch_us,m.config[4]))
if i==indices[0] else m for i,m in enumerate(models))
device_model_words(result)
return result
def gpio_models(dt, spec, windows):
if (dt.get(spec.node, 'compatible').rstrip(b'\0') != b'gpio,t8101' or
dt.get(spec.node, '#gpio-pins') != struct.pack('<I', spec.pins) or
dt.get(spec.node, '#gpio-int-groups') != struct.pack('<I', spec.groups)):
raise ValueError('GPIO node does not match the audited register ABI')
props = dt.nodes[spec.node]
supported = (bytes(props['supported-int-groups'].value)
if 'supported-int-groups' in props else
struct.pack(f'<{spec.groups}I', *range(spec.groups)))
if (len(set(spec.supported)) != len(spec.supported) or
any(g < 0 or g >= spec.groups for g in spec.supported) or
supported != struct.pack(f'<{len(spec.supported)}I', *spec.supported) or
len(windows) != 1):
raise ValueError('unsupported GPIO interrupt groups or register windows')
kind, routes = GPIO_QUIET_V1, []
if len(spec.idle_rx) > 2:
raise ValueError('GPIO model supports at most two idle RX routes')
for rx in spec.idle_rx:
phandle = dt.get(spec.node, 'AAPL,phandle')
if (not 0 <= rx.pin < spec.pins or not 1 <= rx.mux <= 3 or
phandle is None or len(phandle) != 4 or phandle == bytes(4) or
dt.get(rx.peripheral, 'compatible') != b'uart-1,samsung\0' or
dt.get(rx.peripheral, 'uart-version') != struct.pack('<I',1) or
dt.get(rx.peripheral, 'function-rx') !=
phandle+b'OIPG'+struct.pack('<2I',rx.pin,rx.mux+1)):
raise ValueError('GPIO idle RX route differs from the audited UART function')
kind = GPIO_UART_RX_IDLE_V1
routes.append((rx.pin+1) | (rx.mux << 16))
for clkreq in spec.idle_clkreq:
phandle = dt.get(spec.node, 'AAPL,phandle')
if (not 0 <= clkreq.pin < spec.pins or not 1 <= clkreq.mux <= 3 or
phandle is None or len(phandle) != 4 or phandle == bytes(4) or
dt.get(clkreq.bridge, 'compatible') != b'apcie-bridge\0' or
dt.get(clkreq.bridge, 'function-clkreq') !=
phandle+b'OIPG'+struct.pack('<2I',clkreq.pin,clkreq.mux+1)):
raise ValueError('GPIO CLKREQ route differs from the audited PCIe function')
kind = GPIO_IDLE_INPUTS_V1
routes.append((clkreq.pin+1) | (clkreq.mux << 16))
for tx in spec.idle_tx:
phandle = dt.get(spec.node, 'AAPL,phandle')
if (not 0 <= tx.pin < spec.pins or not 1 <= tx.mux <= 3 or
phandle is None or len(phandle) != 4 or phandle == bytes(4) or
dt.get(tx.peripheral, 'compatible') != b'uart-1,samsung\0' or
dt.get(tx.peripheral, 'uart-version') != struct.pack('<I',1) or
dt.get(tx.peripheral, 'function-tx') !=
phandle+b'OIPG'+struct.pack('<2I',tx.pin,0x100 | (tx.mux+1))):
raise ValueError('GPIO idle TX route differs from the audited UART function')
# Peripheral TX is idle high while no UART engine is attached. This
# admits only pad setup, never serial traffic or UART completion.
kind = GPIO_IDLE_INPUTS_V1
routes.append((tx.pin+1) | (tx.mux << 16))
for bus in spec.idle_i2c:
if (spec.node!='/device-tree/arm-io/gpio0' or bus.mux!=1 or
not 0<=bus.scl<spec.pins or not 0<=bus.sda<spec.pins or bus.scl==bus.sda or
dt.get(bus.peripheral,'compatible') != b'i2c,t8101\0i2c,s5l8940x\0iic,soft\0' or
dt.get(bus.peripheral,'gpio-iic_scl') != struct.pack('<3I',bus.scl,0x10102,0x5041) or
dt.get(bus.peripheral,'gpio-iic_sda') != struct.pack('<3I',bus.sda,0x10102,0x5041)):
raise ValueError('GPIO idle I2C pads differ from the audited AP route')
# Released external lines are pulled high. This only admits pad
# routing: no I2C controller, target ACK or transfer is synthesized.
kind=GPIO_IDLE_PADS_V2
routes.extend(((pin+1)|(bus.mux<<16)) for pin in (bus.scl,bus.sda))
if kind == GPIO_IDLE_PADS_V2:
if len(routes)>8:
raise ValueError('GPIO model supports at most eight idle pads')
packed=[(v&0xffff)|((v>>16)<<9) for v in routes]
routes=[sum(v<<(11*i) for i,v in enumerate(packed[j:j+4])) for j in (0,4)]
elif kind == GPIO_IDLE_INPUTS_V1:
if len(routes) > 6:
raise ValueError('GPIO model supports at most six idle inputs')
routes = [sum(v << (18*i) for i,v in enumerate(routes[j:j+3])) for j in (0,3)]
else:
routes += [0]*(2-len(routes))
models = (DeviceModel(spec.node, kind, *windows[0],
(spec.pins, spec.groups, sum(1 << g for g in spec.supported), *routes)),)
device_model_words(models)
return models
# PMGR's BPR word (docs/PMGR-BPR-20261017.md): AppleT8140PMGR reads and writes
# PMGR range 1 +0x5c030 (the get-bpr user client of the crash and panic
# reporters). On the J700 it lies inside the AES node's key window, which model 8
# owns, so that model serves it when the tree places it there.
PMGR_NODE = '/device-tree/arm-io/pmgr'
PMGR_BPR_RANGE, PMGR_BPR_OFFSET = 1, 0x5c030
def aes_pmgr_bpr_offset(dt, spec):
"""The BPR word's offset inside the AES key window, or 0 when the tree does
not put PMGR range 1 +0x5c030 wholly inside it."""
aes, pmgr = dt.get(spec.node, 'reg'), dt.get(PMGR_NODE, 'reg')
if not aes or not pmgr or len(aes) % 16 or len(pmgr) % 16 or \
len(pmgr) // 16 <= PMGR_BPR_RANGE or len(aes) // 16 <= spec.register_index:
return 0
base, size = struct.unpack_from('<QQ', aes, 16 * spec.register_index)
pbase, psize = struct.unpack_from('<QQ', pmgr, 16 * PMGR_BPR_RANGE)
word = pbase + PMGR_BPR_OFFSET
if not (pbase <= base and base + size <= pbase + psize and base <= word and word + 4 <= base + size):
return 0
offset = word - base
return 0 if offset in spec.offsets else offset
def aes_key_models(dt, spec, windows):
if spec.software_dma and spec.engine_version != 5:
raise ValueError('AES DMA requires the audited version-5 engine')
names = ('sioaesdisable-offset', 'sioaesdisableuid1-offset',
'sioaesdisablegid0-offset', 'sioaesdisablegid1-offset')
if (dt.get(spec.node, 'compatible').rstrip(b'\0') != b'aes,s8000' or
len(spec.offsets) != 4 or
any(dt.get(spec.node, name) != struct.pack('<I', value)
for name, value in zip(names, spec.offsets)) or
len(windows) != 3 or not 0 <= spec.register_index < len(windows)):
raise ValueError('AES key controls do not match the audited register ABI')
models = (DeviceModel(spec.node, AES_NO_KEYS_V1, *windows[spec.register_index],
(*spec.offsets, aes_pmgr_bpr_offset(dt, spec))),)
if spec.engine_version is not None:
if (spec.engine_version != 5 or spec.register_index != 1 or
dt.get(spec.node, 'aes-version') != struct.pack('<I', 5) or
dt.get(spec.node, 'address-width') != struct.pack('<I', 42)):
raise ValueError('AES idle engine does not match the audited version-5 ABI')
if spec.software_dma:
mapper='/device-tree/arm-io/dart-sio/mapper-aes'
if (dt.get(spec.node,'interrupts') != struct.pack('<I',1107) or
dt.get(spec.node,'iommu-parent') != struct.pack('<I',0x84) or
dt.get(mapper,'AAPL,phandle') != struct.pack('<I',0x84) or
dt.get(mapper,'compatible') != b'iommu-mapper\0' or
dt.get('/device-tree/arm-io/dart-sio','sid') != struct.pack('<II',0,1)):
raise ValueError('AES DMA does not match the audited SIO mapper and IRQ')
models += (DeviceModel(spec.node,AES_SOFTWARE_V1,*windows[0],(5,42,1107,21,1)),)
else:
models += (DeviceModel(spec.node, AES_IDLE_V1, *windows[0], (5,0,0,0,0)),)
device_model_words(models)
return models
def watchdog_models(dt, spec, windows, frequency):
"""Expose primary WD1 and the SMC deadline's audited zero-write disarm.
The model consumes samples in the same reference domain as the guest
counter. A different peripheral clock needs an explicit clock adapter.
SMC arming and other auxiliary words remain unsupported.
"""
if (dt.get(spec.node, 'compatible').split(b'\0')[0] != spec.compatible or
spec.compatible != b'wdt,t8101' or spec.version != 2 or
dt.get(spec.node, 'wdt-version') != struct.pack('<I', spec.version) or
dt.get(spec.node, 'clock-ids') != struct.pack('<I', spec.clock_id)):
raise ValueError('watchdog node does not match the audited register ABI')
if frequency != spec.frequency:
raise ValueError('watchdog reference clock differs from the guest counter')
if len(windows) != 5 or windows[0][1] != 0x4000 or any(n != 4 for _, n in windows[1:]):
raise ValueError('unsupported watchdog register windows')
base, span = windows[0]
if tuple((b-base,n) for b,n in windows[1:]) != ((0xc224,4),(0x8008,4),(0x802c,4),(0x8020,4)):
raise ValueError('watchdog auxiliary register layout changed')
models = (DeviceModel(spec.node, WATCHDOG_WD1_V1, base, span,
(spec.frequency, 0, 0, 0, 0)),
DeviceModel(spec.node, WATCHDOG_SMC_DISARM_V1, base+0x8000, 0x1000,
(8, 0, 0, 0, 0)))
device_model_words(models)
return models
def aic_models(dt, spec, windows):
"""Publish the flat-bank AIC3 aperture with matching EVENT ABI.
Geometry describes a virtual controller, not measured T8140 counts.
Mask/pending/EVENT state lives in the portable model; the host adapter
drives one guest IRQ endpoint. Additional targets and delay groups remain
unsupported. Virtual source producers are connected separately.
"""
props = dt.nodes[spec.node]
if dt.get(spec.node, 'compatible').rstrip(b'\0') != spec.compatible:
raise ValueError('AIC node does not match the audited register ABI')
for name, expected in {'rev-offset': 0, 'cap0-offset': 4, 'maxnumirq-offset': 12,
'extint-baseaddress': 0x10000, 'aicglbcfg-offset': 0x14,
'extintrcfg-stride': 0x4a00, 'intmaskset-stride': 0x4a00,
'intmaskclear-stride': 0x4a00, 'hwintmon-stride': 0x4a00,
'cap0_max_interrupt_umask': 0xffff,
'max_irq_interrupt_umask': 0xffff,
'#interrupt-cells': 1}.items():
if dt.get(spec.node, name) != struct.pack('<I', expected):
raise ValueError(f'unsupported AIC property {name}')
# The virtual revision implements the flat AIC3 banks. Revision >= 0xb
# selects additional die/SOC windows in the audited Darwin driver.
if spec.revision != 0xa:
raise ValueError('unsupported virtual AIC revision')
for name in ('aic-numdies', 'aic-numdies-per-die',
'aic-die-offset-stride', 'aic-soc-offset-stride', 'max-extint-num'):
if name in props and (len(props[name].value) not in (4, 8) or
any(props[name].value)):
raise ValueError(f'unsupported AIC topology property {name}')
if 'aic-rev' in props and props['aic-rev'].value not in (
struct.pack('<I', 0), struct.pack('<I', spec.revision)):
raise ValueError('AIC revision property conflicts with the model')
phandle = dt.get(spec.node, 'AAPL,phandle')
if len(phandle) != 4:
raise ValueError('invalid AIC phandle')
for path, node in dt.nodes.items():
if 'interrupt-parent' not in node or node['interrupt-parent'].value != phandle:
continue
irqs = node.get('interrupts')
if irqs is not None and (len(irqs.value) % 4 or any(
irq >= spec.irq_count for (irq,) in struct.iter_unpack('<I', irqs.value))):
raise ValueError(f'AIC virtual capacity does not cover {path}')
if len(windows) != 1:
raise ValueError('unsupported AIC window count')
base, span = windows[0]
models = (DeviceModel(spec.node, AIC3_FLAT_V1, base, span,
(spec.irq_count, 4096, spec.event_offset, spec.revision, 0)),)
device_model_words(models)
event = struct.pack('<Q', spec.event_offset)
if 'aic-iack-offset' in props and props['aic-iack-offset'].value != event:
raise ValueError('AIC EVENT property conflicts with the model')
dt.put(spec.node, 'aic-iack-offset', event)
return models
def mcc_models(dt, spec, windows):
"""A coherent virtual memory backend, with no Apple-managed private cache.
Supplies cache capacity/quotas and a per-DCS DRAM policy latch. Physical power,
flush, protection and error-control registers remain separate contracts.
"""
def u32(name):
value = dt.get(spec.node, name)
if len(value) != 4:
raise ValueError(f'{spec.node}/{name} must be one u32')
return struct.unpack('<I', value)[0]
if dt.get(spec.node, 'compatible').rstrip(b'\0') != spec.compatible:
raise ValueError('MCC node does not match the audited register ABI')
amcc, dcs = u32('amcc-count'), u32('dcs-count-per-amcc')
if (amcc, dcs) != (spec.amcc_count, spec.dcs_count) or not 1 <= dcs <= 4:
raise ValueError('unsupported MCC bank geometry')
first = u32('amcc-reg-idx')
dcs_first = u32('dcs-reg-idx')
socni_first, socni_count = u32('socni-reg-idx'), u32('socni-count')
if socni_count != len(spec.socni_counters):
raise ValueError('unsupported SOCNI counter geometry')
if (not amcc or first + amcc > len(windows) or
dcs_first + amcc*dcs > len(windows) or socni_first + socni_count > len(windows)):
raise ValueError('MCC register indices exceed its device-tree windows')
models = tuple(DeviceModel(spec.node, MCC_COHERENT_V1, pa, size, (dcs, 0, 0, 0, 0))
for pa, size in windows[first:first+amcc])
models += tuple(DeviceModel(spec.node, MCC_DCS_COHERENT_V1, pa, size, (0, 0, 0, 0, 0))
for pa, size in windows[dcs_first:dcs_first+amcc*dcs])
models += tuple(DeviceModel(spec.node, MCC_SOCNI_V1, pa, size, (count, 0, 0, 0, 0))
for (pa, size), count in zip(windows[socni_first:socni_first+socni_count],
spec.socni_counters))
device_model_words(models)
return models
def mcc_error_models(dt, node, windows, models):
"""Extend DCS ownership only within the declared platform error aperture.
Fifteen independently decoded enable words span 0x38054 bytes per DCS;
holes and neighboring error status words remain unimplemented.
"""
if (dt.get(node, 'compatible') != b'error-handler,t8140\0' or not windows or
not any(m.kind == MCC_COHERENT_V1 and m.base == windows[0][0] for m in models)):
raise ValueError('MCC error handler does not match the audited aperture')
result=[]
for model in models:
if model.kind == MCC_DCS_COHERENT_V1:
if model.span != 0x8000 or not any(
base <= model.base and model.base+0x39000 <= base+size
for base,size in windows):
raise ValueError('DCS error registers are outside the declared platform aperture')
model=replace(model,span=0x39000)
result.append(model)
device_model_words(result)
return tuple(result)
def socni_error_models(dt, node, windows):
if (dt.get(node, 'compatible') != b'error-handler,t8140\0' or len(windows) < 15 or
any(size != 0x4000 for _,size in windows[9:15])):
raise ValueError('SOCNI error handler lacks its six policy windows')
# The selected binary's six report tables follow DT reg indices 9..14.
# Their relative placement is audited; the bus supplies physical bases.
base=windows[9][0]
if tuple(pa-base for pa,_ in windows[9:15]) != (0,0x1000000,0x2000000,
0x3000000,0x10000000,0x20000000):
raise ValueError('unsupported SOCNI error bank geometry')
models=tuple(DeviceModel(node,SOCNI_ERRORS_V1,pa,size,(i,0,0,0,0))
for i,(pa,size) in enumerate(windows[9:15]))
device_model_words(models)
return models
def platform_error_models(dt, node, windows):
if (dt.get(node, 'compatible') != b'error-handler,t8140\0' or len(windows) != 17 or
windows[15][1] != 0x9a4 or windows[16][1] != 0x20000):
raise ValueError('platform error policy windows differ from the audited ABI')
models=(DeviceModel(node,PLATFORM_ERROR_POLICY_V1,windows[15][0],0x1000,(0x104,1,0,0,0)),
DeviceModel(node,PLATFORM_ERROR_POLICY_V1,*windows[16],(0x4084,0,0,0,0)))
device_model_words(models)
return models
def mcc_ptd_models(dt, spec, windows):
"""Resolve the audited MCC protocol through the firmware's PTD directory.
Register placement and PTD indices belong to the guest profile. A single
descriptor joins the database and update aliases to one virtual state.
"""
if dt.get(spec.node, 'compatible').rstrip(b'\0') != spec.compatible:
raise ValueError('MCC PTD node does not match the audited register ABI')
def u32(name):
value = dt.get(spec.node, name)
if len(value) != 4:
raise ValueError(f'invalid MCC PTD property {name}')
return struct.unpack('<I', value)[0]
db_index, update_index = u32('pmp_ptd_db_reg_idx'), u32('pmp_ptd_update_reg_idx')
if max(db_index, update_index) >= len(windows):
raise ValueError('MCC PTD register index outside window list')
(base, db_size), (update, update_size) = windows[db_index], windows[update_index]
if db_size != 0x4000 or update_size != 0x4000 or update < base+db_size:
raise ValueError('unsupported MCC PTD aperture geometry')
data = dt.get(spec.pmp_node, 'ptd-range')
if not data or len(data) % 32:
raise ValueError('invalid PMP PTD directory')
entries = {}
for ident, first, count, flags, name in struct.iter_unpack('<IIII16s', data):
if ident in entries:
raise ValueError('duplicate PMP PTD range ID')
entries[ident] = (first, count, flags, name.rstrip(b'\0'))
ranges = []
for prop, expected_name, extra in (
('ptd-range-drv2ctrl', b'MC-DRV-TO-CTRL', 2),
('ptd-range-ctrl2drv', b'MC-CTRL-TO-DRV', 1)):
entry = entries.get(u32(prop))
if (entry is None or entry[1:] != (spec.quota_words+extra, 0, expected_name)):
raise ValueError('unsupported MCC PTD range contract')
ranges.append(entry[0])
models = (DeviceModel(spec.node, MCC_PTD_COHERENT_V1, base, update+update_size-base,
(update-base, *ranges, spec.quota_words, 0)),)
device_model_words(models)
return models
def pmp_active_models(dt, models):
"""Bind PMP physical-domain votes to the registered PMGR backends.
Preserve the shared MCC data/update aperture and supported row mask.
AVD/ISP/PRORES/VENC/MSR/JPG/GFX own power/vote transitions and state-derived ACKs; other
covered domains remain active. PMP-STATUS describes successful virtual
registry initialization. Virtual devices, additional voters and DVFS
packets need separate contracts.
"""
pmgr='/device-tree/arm-io/pmgr'
pmp='/device-tree/arm-io/pmp/iop-pmp-nub'
for path,name,digest in (
(pmgr,'devices','695eaa2f3def5f2fe9e7e8a97306b2ba44ff2f246b97c4f80a8c46bd496aa384'),
(pmgr,'reg','228f2f75c199e9c0cdb99d16d484c2dade36d02673f8826d062e195de2b446f3'),
(pmgr,'ptd-ranges','badb95d4143a7953cf071ff545271711fd754331253e5d2b966a4f47459ac69e'),
(pmp,'ptd-range','2114f1290101b1d573fcae3ac905ccc322f30ee1e125fa54511bd1e825bcc29a'),
(pmp,'soc-device','d9332c697501717107223fcf3b7fb500ecf7b7bd64d73d1afed7084b0e1e1ba1')):
prop=dt.nodes.get(path,{}).get(name)
if (prop is None or prop.placeholder or
hashlib.sha256(bytes(prop.value)).hexdigest()!=digest):
raise ValueError(f'PMP active-domain {name} contract changed')
for path,name,expected in (
(pmgr,'pmp',struct.pack('<I',2)),
(pmgr,'pmp-voter-agent-idx',struct.pack('<I',0)),
(pmp,'ptd-driver-version',struct.pack('<I',1)),
(pmp,'soc-device-ps-group',struct.pack('<Q',0x080fff80))):
prop=dt.nodes.get(path,{}).get(name)
if prop is None or prop.placeholder or prop.value!=expected:
raise ValueError(f'PMP active-domain {name} topology changed')
physical={row[3] for row in (dt.get(pmgr,'devices')[i:i+48]
for i in range(0,len(dt.get(pmgr,'devices')),48)) if not row[0]&16 and row[3]}
rows=tuple(struct.iter_unpack('<31I',dt.get(pmp,'soc-device')))
mask=sum(1<<i for i,row in enumerate(rows) if row[0] in physical)
if mask!=PMP_ACTIVE_MASK:
raise ValueError('PMP active-domain row mapping differs from the backend')
old=[i for i,m in enumerate(models) if m.kind==MCC_PTD_COHERENT_V1]
if len(old)!=1 or models[old[0]].words()[1:]!=(0x3003c0000,0x14000,0x10000,0x1a8,0x1b8,8,0):
raise ValueError('PMP active-domain shared PTD aperture changed')
result=list(models)
result[old[0]]=replace(result[old[0]],kind=PTD_PMP_ACTIVE_V1,
config=(0x10000,0x1a8,0x1b8,8,mask))
device_model_words(result)
return tuple(result)
def _gpio_routes_valid(model):
routes = model.config[3:]
if model.kind == GPIO_QUIET_V1:
return not any(routes)
if model.kind == GPIO_IDLE_INPUTS_V1:
if any(word >> 54 for word in routes):
return False
routes = tuple((word >> (18*i)) & 0x3ffff for word in routes for i in range(3))
elif model.kind == GPIO_IDLE_PADS_V2:
if any(word>>44 for word in routes):return False
raw=tuple((word>>(11*i))&0x7ff for word in routes for i in range(4))
routes=tuple((v&0x1ff)|((v>>9)<<16) for v in raw)
if model.kind not in (GPIO_UART_RX_IDLE_V1, GPIO_IDLE_INPUTS_V1, GPIO_IDLE_PADS_V2) or not routes[0]:
return False
pins = []
for route in routes:
if not route:
continue
pin, mux = route & 0xffff, route >> 16
if not 1 <= pin <= model.config[0] or not 1 <= mux <= 3 or pin in pins:
return False
pins.append(pin)
return True
def device_model_words(models, excluded=()):
"""Validate the whole registry before publishing a fixed-size ABI block."""
if len(models) > MODEL_MAX:
raise ValueError('too many virtual device models')
used = list(excluded)
if any(type(b) is not int or type(n) is not int or b < 0 or n < 0 or
b+n >= 1 << 64 for b, n in used):
raise ValueError('invalid excluded memory range')
words = [len(models)]
for model in models:
values = model.words()
if len(values) != MODEL_WORDS or any(type(v) is not int or not 0 <= v < 1 << 64 for v in values):
raise ValueError('malformed virtual device descriptor')
if model.kind == MCC_COHERENT_V1 and 1 <= model.config[0] <= 4:
minimum = max((model.config[0]-1)*0x40000 + 0x2e008, 0x102804)
elif model.kind == MCC_DCS_COHERENT_V1 and model.config[0] == 0:
minimum = 0x4070
elif model.kind == MCC_SOCNI_V1 and 1 <= model.config[0] <= 64:
minimum = 0x4000 + model.config[0]*8
elif model.kind == WATCHDOG_WD1_V1 and 0 < model.config[0] <= 0xffffffff:
minimum = 0x20
elif (model.kind == WATCHDOG_SMC_DISARM_V1 and model.span == 0x1000 and
model.config[0] == 8):
minimum = model.span
elif (model.kind == AOP_TIMEBASE_V1 and model.span == 0x1000 and
model.config[0] == 24000000):
minimum = 0x1000
elif (model.kind == PMGR_ACTIVE_BOOT_V1 and model.config[0] in (0,1) and
model.span == (0x11000,0x9000)[model.config[0]] and
model.config[1] in ((0,) if model.config[0] else (0,PMGR_CPU0_BOOT_V1)) and
not any(model.config[2:])):
minimum = model.span
elif (model.kind == PMGR_AON_FUSES_V1 and model.span == 0x4000 and
model.config[0] == 0):
minimum = model.span
elif (model.kind == PMGR_CPU_PERF_BOOT_V1 and model.span == 0x1000 and
model.config[0] in (0,1)):
minimum = model.span
elif (model.kind == PMGR_DVMR_WEIGHTS_V1 and model.span == 0x1000 and
model.config[0] in (0,1)):
minimum = model.span
elif (model.kind == PMGR_AON_COUNTER_CONFIG_V1 and model.span == 0x1000 and
model.config[0] in (6,7)):
minimum = model.span
elif (model.kind == PMGR_SOC_PERF_RESTORE_V1 and 0<=model.config[0]<=5 and
model.span == (0x2000 if model.config[0]==0 else 0x3000 if model.config[0]==1 else 0x1000)):
minimum = model.span
elif (model.kind == PMGR_ACC_PERF_RESTORE_V1 and model.config[0] in (0,1)
and model.span == 0x2000):
minimum = model.span
elif (model.kind == PMGR_ACC_BOOT_POLICY_V1 and model.span == 0x1000 and
model.config[0] in (0,1)):
minimum = model.span
elif (model.kind == PMGR_LLT_DISABLED_V1 and model.base == 0x300740000 and
model.span == 0x1000 and model.config[0] == 0):
minimum = model.span
elif (model.kind == CORESIGHT_LOCK_V1 and model.span == 0x1000 and
0 <= model.config[0] < 6 and model.base ==
(0x210010000+model.config[0]*0x100000 if model.config[0]<4 else
0x211010000+(model.config[0]-4)*0x100000)):
minimum = model.span
elif (model.kind == QSPI_BOOT_V1 and model.span == 0x5000 and model.config[0] == 0):
minimum = model.span
elif (model.kind == DWI_STARTUP_V1 and model.base == 0x300200000 and
model.span == 0x4000 and model.config[0] == 0):
minimum = model.span
elif (model.kind == I2C_EMPTY_V1 and model.base == 0x385018000 and
model.span == 0x4000 and model.config == (2,6,1110,0,0)):
minimum = model.span
elif (model.kind == SEP_ASC_MAILBOX_V1 and model.base == 0x282600000 and
model.span == 0x9000 and model.config in ((8,0,0,0,0),(8,1,346,343,0),(8,1,346,343,1),(8,1,346,343,3),(8,1,346,343,7),(8,1,346,343,11),(8,1,346,343,15),(8,1,346,343,19),(8,1,346,343,27),(8,1,346,343,59))):
minimum = model.span
elif (model.kind in (ANS_ASC_V1, ANS_NVME_V1) and
(model.base, model.span) == (ANS_NVME_WINDOW if model.kind == ANS_NVME_V1 else ANS_ASC_WINDOW) and
0 < model.config[0] < (1 << 36)-ANS_DISK_OFFSET-ANS_DISK_MAX and not model.config[0] % 0x4000 and
not model.config[4] and
(model.config[1:4] == (1010, 1007, 0) if model.kind == ANS_ASC_V1 else
model.config[2] == 1039 and
((model.config[3] == ANS_WINDOW_MODE and not model.config[0] % (1 << 20) and
model.config[1] >= 1 << 30 and not model.config[1] % (1 << 20)) or
(model.config[3] == 0 and 0 < model.config[1] <= ANS_DISK_MAX and
not model.config[1] % 0x1000)))):
minimum = model.span
elif (model.kind == PMGR_CPU_CLUSTER_ACTIVE_V1 and model.span == 0x1000 and
model.config[0] in (0,PMGR_AVE_CLUSTER_V1)):
minimum = model.span
elif (model.kind in (PMGR_AP_WAKE_MASK_V1, PTD_IRQ_CONFIG_V1,
PMGR_AVE_TVM_IDLE_V1, PMGR_SOC_GATE_POLICY_V1,
PMGR_AXI2AF_POLICY_V1, PMC_ENABLE_POLICY_V1,
PMC_RMBS_REQUEST_V1, PMGR_ENERGY_RESTORE_V1) and model.span == 0x1000 and
model.config[0] == 0):
minimum = model.span
elif (model.kind == PMGR_SOC_DPE_POLICY_V1 and model.span == 0x3000 and
model.config[0] == 0):
minimum = model.span
elif (model.kind == PMC_DVMR_CONFIG_V1 and model.span == 0x6000 and
model.config[0] == 0):
minimum = model.span
elif (model.kind == DART_T8110_V1 and model.span in (0xc000,0x20000) and
1<=model.config[0]<=256 and
(model.config[1]==44 or (model.config[1]==DART_SIO_BOOT_V1 and
model.config[0]==16 and model.span==0x20000 and model.config[4] and
not model.config[4]&(model.config[4]-1)) or
(model.config[1]==DART_SEP_BOOT_V1 and model.base==0x280b80000 and
model.span==0x20000 and model.config[0]==3 and model.config[2]==353 and
model.config[4]==1<<20) or
(model.config[1]==DART_ADRENO_BOOT_V1 and model.span==0x20000 and
model.config[0]==16 and model.config[4]==1<<31) or
(model.config[1]==DART_ANE_INIT_V1 and
(model.base,model.span) in ((0x401800000,0xc000),
(0x401820000,0x20000),(0x401840000,0x20000)) and
model.config[0]==16 and model.config[2]==656 and model.config[4]==1)) and
model.config[2]<4096 and model.config[3]==36 and model.config[4]<1<<32):
minimum = model.span
elif (model.kind in (SPMI_GEN3_EMPTY_V1,SPMI_PMU_RTC_V1) and model.config[0] >= 0x8000 and
model.config[0] % 0x4000 == 0 and model.span == model.config[0]+0x4000 and
0x4000 <= model.config[1] <= model.config[0]-0x4000 and
model.config[1] % 0x4000 == 0 and model.config[2] < 4096 and
((model.kind==SPMI_GEN3_EMPTY_V1 and model.config[3:]==(256,256)) or
(model.kind==SPMI_PMU_RTC_V1 and model.base==0x308700000 and
model.config[:3]==(0x14000,0x4000,487) and
0<=model.config[3]<=PMU_RTC_LIMIT_US and model.config[4]==PMU_RTC_CONTRACT))):
minimum = model.config[0]+0x4000
elif (model.kind in (GPIO_QUIET_V1, GPIO_UART_RX_IDLE_V1, GPIO_IDLE_INPUTS_V1, GPIO_IDLE_PADS_V2) and
1 <= model.config[0] <= 256 and
1 <= model.config[1] <= 7 and 0 < model.config[2] < 1 << model.config[1] and
_gpio_routes_valid(model)):
minimum = 0x800 + model.config[1]*0x40
elif (model.kind == DOCK_ABSENT_V1 and model.span == 0x1000 and
model.config[0] % 4 == 0 and model.config[0] <= 0xffc and
0 < model.config[1] <= 0xffffffff and
not model.config[1] & (model.config[1]-1) and not any(model.config[2:])):
minimum = 0x1000
elif (model.kind == DOCK_FIFO_V1 and model.span == 0x35000 and
model.config[:2] == (0x14000, 0x28000) and model.config[2] < 4096 and
model.config[3:] == (2048, 0)):
minimum = 0x35000
elif model.kind == SOCNI_ERRORS_V1 and model.span == 0x4000 and model.config[0] < 6:
minimum = 0x4000
elif (model.kind == PLATFORM_ERROR_POLICY_V1 and not any(model.config[2:]) and
((model.span == 0x1000 and model.config[:2] == (0x104,1)) or
(model.span == 0x20000 and model.config[:2] == (0x4084,0)))):
minimum = model.span
elif (model.kind == AES_IDLE_V1 and model.span == 0x4000 and
model.config == (5,0,0,0,0)):
minimum = model.span
elif (model.kind == AES_SOFTWARE_V1 and model.span == 0x4000 and
model.config == (5,42,1107,21,1)):
minimum = model.span
elif (model.kind == AES_NO_KEYS_V1 and
len(set(model.config[:4])) == 4 and
all(v % 4 == 0 for v in model.config[:4]) and
(not model.config[4] or (model.config[4] % 4 == 0 and
model.config[4] not in model.config[:4]))):
# config[4]: zero, or PMGR's BPR word (docs/PMGR-BPR-20261017.md).
minimum = max(model.config)+4
elif (model.kind == PTD_PMP_ACTIVE_V1 and model.span == 0x14000 and
model.config == (0x10000,0x1a8,0x1b8,8,PMP_ACTIVE_MASK)):
minimum = model.span
elif (model.kind == MCC_PTD_COHERENT_V1 and
model.config[0] >= 0x4000 and model.config[0] % 0x1000 == 0 and
model.span == model.config[0]+0x4000 and 1 <= model.config[3] <= 8 and
model.config[1] <= 1024-model.config[3]-2 and
model.config[2] <= 1024-model.config[3]-1 and not model.config[4] and
(model.config[1]+model.config[3]+2 <= model.config[2] or
model.config[2]+model.config[3]+1 <= model.config[1])):
minimum = model.config[0]+0x4000
elif (model.kind == AIC3_FLAT_V1 and 0 < model.config[0] <= 4096 and
model.config[0] % 32 == 0 and model.config[1] == 4096 and
model.config[2] >= 0x14a00 and model.config[2] % 4 == 0 and
model.config[3] == 0xa and model.config[4] == 0):
minimum = model.config[2] + 4
else:
raise ValueError('unsupported virtual device configuration')
if model.kind not in (AIC3_FLAT_V1, MCC_PTD_COHERENT_V1, PTD_PMP_ACTIVE_V1,
GPIO_QUIET_V1, AES_NO_KEYS_V1, SPMI_GEN3_EMPTY_V1, SPMI_PMU_RTC_V1,
DOCK_ABSENT_V1, GPIO_UART_RX_IDLE_V1,
GPIO_IDLE_INPUTS_V1, GPIO_IDLE_PADS_V2, DOCK_FIFO_V1,
PLATFORM_ERROR_POLICY_V1, DART_T8110_V1, PMGR_ACTIVE_BOOT_V1,
I2C_EMPTY_V1, AES_SOFTWARE_V1, SEP_ASC_MAILBOX_V1,
ANS_ASC_V1, ANS_NVME_V1) and any(model.config[1:]):
raise ValueError('unsupported virtual device configuration')
if (not model.span or (model.base | model.span) & 0xfff or
model.base + model.span > 1 << 36 or
model.span < minimum):
raise ValueError('invalid virtual device window')
if any(n and model.base < b+n and b < model.base+model.span for b, n in used):
raise ValueError('virtual device window overlaps another device or owned memory')
used.append((model.base, model.span))
words.extend(values)
if any(m.kind==PTD_PMP_ACTIVE_V1 for m in models):
for group,base,span in ((0,0x300700000,0x11000),(1,0x308280000,0x9000)):
if not any(m.kind==PMGR_ACTIVE_BOOT_V1 and (m.base,m.span,m.config[0])==(base,span,group)
for m in models):
raise ValueError('PMP votes require both registered active PMGR backends')
if any(m.kind==PMGR_ACTIVE_BOOT_V1 and m.config[1]==PMGR_CPU0_BOOT_V1 for m in models):
required={(PMGR_ACTIVE_BOOT_V1,0x300700000,0x11000,0,1,0,0,0),
(PMGR_ACTIVE_BOOT_V1,0x308280000,0x9000,1,0,0,0,0)}
if not required<={m.words() for m in models} or not any(
m.kind==PMGR_CPU_CLUSTER_ACTIVE_V1 and m.base==0x300780000 and
m.span==0x1000 for m in models):
raise ValueError('CPU boot power policy requires both PMGR banks and the active cluster controls')
if any(m.kind==PMGR_AVE_TVM_IDLE_V1 or
(m.kind==PMGR_CPU_CLUSTER_ACTIVE_V1 and m.config[0]) for m in models):
required={(PMGR_CPU_CLUSTER_ACTIVE_V1,0x300780000,0x1000,1,0,0,0,0),
(PMGR_AVE_TVM_IDLE_V1,0x300760000,0x1000,0,0,0,0,0)}
if not required<={m.words() for m in models}:
raise ValueError('AVE cluster policy requires its exact cluster and TVM owners')
ans = [m for m in models if m.kind in (ANS_ASC_V1, ANS_NVME_V1)]
if ans:
# One pair sharing one reservation that misses every owned range
# (compat/device.c device_models_valid makes the same check).
if (sorted(m.kind for m in ans) != [ANS_ASC_V1, ANS_NVME_V1] or
len({m.config[0] for m in ans}) != 1):
raise ValueError('virtual ANS2 needs exactly one ASC and NVMe pair on one reservation')
nvme = next(m for m in ans if m.kind == ANS_NVME_V1)
base = ans[0].config[0]
span = ANS_WINDOW_BYTES if nvme.config[3] == ANS_WINDOW_MODE else ANS_DISK_OFFSET + nvme.config[1]
if any(n and base < b+n and b < base+span for b, n in list(excluded)+used[len(excluded):]):
raise ValueError('virtual ANS2 reservation overlaps owned memory or a device')
words.extend([0] * ((MODEL_MAX-len(models))*MODEL_WORDS))
return words