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.
1635 lines
85 KiB
Python
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
|