Files
Snapintosh/tools/audit-secondary-runtime.py
T
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

1356 lines
92 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Read-only ownership, private-map and runtime audit after an SMP boot slice.
Unlike the historical CPU0-only device checkpoint, this permits a secondary
to have executed. It reconstructs CPU/DMA references and validates unused
runtime banks separately from the executor that actually ran.
"""
import argparse
import hashlib
import json
from pathlib import Path
import re
import runpy
import struct
import subprocess
import sys
ROOT=Path(__file__).resolve().parents[1]
sys.path[:0]=[str(ROOT/'probes'),str(ROOT/'tools')]
import run
import xnuboot as xb
from appledt import DeviceTree
from stage2audit import audit_private_stage2
from ramdisk import ROOT_HASH_NODE
from audit_sep_rom import audit_history as audit_rom_history, audit_buffers as audit_rom_buffers, audit_bootstrap
from audit_sep_pnon import audit_buffers as audit_pnon_buffers
from audit_sep_xars import audit_buffers as audit_xars_buffers
from audit_sep_sks import decode as decode_sks, STATE_BYTES as SKS_STATE_BYTES
# Loader-side arena extent; the compiled registry layout must agree (checked below).
TXM_ARENA_BYTES=xb.TXM_ASPACE_PAGES*0x4000
TXM_ASPACE_LIMIT=TXM_ARENA_BYTES//256
def main():
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('--device',required=True);ap.add_argument('--prefix',required=True)
ap.add_argument('--guest-profile',default='j700ap-26A428',
help='profile used for the resident boot, including board policy')
ap.add_argument('--expect-deferred',action='store_true',
help='verify resident firmware omits the selected absent-device subtrees')
ap.add_argument('--expect-ramdisk',action='store_true',
help='require a reserved raw APFS RAMDisk and its selected boot contract')
ap.add_argument('--expect-isc',action='store_true',
help='require both GPT copies and a separate ISC APFS partition')
ap.add_argument('--ramdisk-mode', choices=('md0', 'iokit'), default='md0',
help='expected resident RAMDisk firmware contract')
ap.add_argument('--expect-root-hash',action='store_true',
help='require the profile-bound active-root hash payload in resident firmware')
ap.add_argument('--allow-unlaunched',action='store_true',
help='audit an early CPU0 stop with no secondaries launched')
ap.add_argument('--diagnose-primary-timeout',action='store_true',
help='retain memory evidence after CPU0 service timeout; does not qualify a clean runtime stop')
ap.add_argument('--diagnose-user-mapping-mismatches',action='store_true',
help='record user mapping/PTE mismatches for stopped-state diagnosis; still exits unsuccessfully on mismatches')
ap.add_argument('--auxkc',help='the pinned linked AuxKC the run loaded (linked-collection profiles)')
ap.add_argument('--trust-additions',help='the CDHash rows an augmented trust-cache run appended')
args=ap.parse_args()
xb.configure_guest(args.guest_profile)
if bool(args.auxkc)!=xb.GUEST_PROFILE.aux_collection_linked:
ap.error('--auxkc is required exactly for linked-collection profiles')
trust_spec=xb.trust_cache_spec(args.trust_additions)
if not re.fullmatch(r'[a-z0-9-]+',args.prefix):raise ValueError('invalid artifact prefix')
layout=subprocess.run(['clang','--target=aarch64-unknown-none-elf','-ffreestanding',
'-mgeneral-regs-only','-Ishadow','-Icompat','-Xclang','-fdump-record-layouts',
'-fsyntax-only','probes/xnuboot.c'],cwd=ROOT,capture_output=True,text=True,check=True).stdout
def offset(kind,field,nested=False):
block=next(b for b in layout.split('*** Dumping AST Record Layout')
if re.search(r'^\s*0 \| struct '+kind+r'\n',b))
indent=r'\s+' if nested else ' '
matches=re.findall(r'^\s*(\d+) \|'+indent+r'\S.* '+field+r'$',block,re.M)
assert len(matches)==1, (kind,field,matches)
return int(matches[0])
refs=runpy.run_path(str(ROOT/'tools/audit-sptm-refs.py'))
darts=runpy.run_path(str(ROOT/'tools/audit-dart-tables.py'))
secondary=runpy.run_path(str(ROOT/'tools/secondary-runtime.py'))
proxy=run.connect(args.device,tries=1)
try:
info=proxy.bootinfo();work, work_size = run.resident_work(proxy, info)
end=work + work_size
table_end=end
def read(pa,n):
if not n>=0:raise ValueError('invalid length')
return b''.join(proxy.readmem(pa+i,min(65536,n-i),live=True) for i in range(0,n,65536))
def q(pa):return struct.unpack('<Q',read(pa,8))[0]
# Bind the running monitor to the just-built executable, not a stale log.
blob=(ROOT/'build/xnuboot.bin').read_bytes()
assert read(info['heap_base']+run.CODE_AT,len(blob))==blob
param_pa=q(work);words=struct.unpack(f'<{xb.PARAMS_WORDS}Q',read(param_pa,xb.PARAMS_WORDS*8))
def param(name):return words[offset('params',name)//8]
txm_native_report=dict(enabled=False)
txm_dynamic_expected=None
if re.search(r' txm_native$',layout,re.M):
at=offset('params','txm_native')//8
native_params=words[at:at+4]
if native_params[0]:
import txmnative
assert native_params[0]==txmnative.HELPER_BASE and native_params[2]==txmnative.WORK_BYTES
assert native_params[1]+native_params[2]==end and not native_params[1]&0x3fff
table_end=native_params[1]
assert q(work+offset('context','work_end'))==table_end
native_record=json.loads((ROOT/'build'/f'{args.prefix}-txm-native.json').read_text())
assert native_record['image_sha256']==txmnative.IMAGE_SHA
assert native_record['generation']==info['stage_generation']
assert int(native_record['work'],16)==table_end
assert native_record['source_sha256']==hashlib.sha256((ROOT/'probes/txmnative.py').read_bytes()).hexdigest()
for segment in native_record['code']:
assert hashlib.sha256(read(native_params[0]+segment['offset'],segment['bytes'])).hexdigest()==segment['sha256']
native_keys=('phase','calls','loads','auth_failures','last_entry','last_status','panic_code','panic_arg','payload_size','manifest_size')
ns=dict(zip(native_keys,struct.unpack('<10Q',read(work+offset('context','txm_native'),80))))
ls=dict(zip(('calls','successes','failures','last_status','payload_pa','manifest_pa','copied_bytes'),
struct.unpack('<7Q',read(work+offset('context','txm_load'),56))))
assert ns['phase']==1 and ns['calls']==ns['loads']+1 and not ns['panic_code'] and not ns['panic_arg']
assert ls['calls']==ns['loads']==ls['successes']+ls['failures']
assert ls['successes'] in (0,1,2,3)
if ls['successes']:
txm_dynamic_expected=[]
for typ,prefix,digest in [
(13,'installed-image4-identity-300s-20260926','da62f681a539e4384dd815f84dec898c3e570cbccd00318d6d9e5e0a03bdb111'),
(14,'installed-txm-load-300s-20260926','3713854a229f36e7d79640c0c5c0c3f9158fdc2d6189c33904b9e754756f0f7d'),
(27,'installed-txm-dual-300s-20260926','8613fd4e512d91c94ef09283c6891b84f6b2df3ffb13a6db74628ad41901fac7')][:ls['successes']]:
captured=json.loads((ROOT/f'build/{prefix}-txm12-input.json').read_text())
original=bytes.fromhex(captured['payload']['hex'])
assert captured['cache_type']==typ and hashlib.sha256(original).hexdigest()==digest
txm_dynamic_expected.append((typ,original[23:]))
assert ls['copied_bytes']==sum(len(data) for _,data in txm_dynamic_expected)
txm_native_report=dict(enabled=True,params=[hex(v) for v in native_params],state=ns,load=ls,
executable_bytes_verified=True,workspace_outside_table_pool=True)
def aux_range():
if not re.search(r' auxkc$',layout,re.M):return (0,0,0,0)
base=offset('params','auxkc')//8
return tuple(words[base:base+4])
import nativecalls as native_api
native_mode=bool(param('stub_count')&native_api.FLAG)
hvc_mode=bool(param('stub_count')&native_api.HVC_FLAG)
assert not hvc_mode or native_mode
native_report=dict(enabled=native_mode,mode='hvc' if hvc_mode else 'private')
writer_enabled=q(work+offset('context','writer_breakpoints_enabled'))
assert writer_enabled in (0,1) and (not writer_enabled or native_mode)
writer_api=runpy.run_path(str(ROOT/'tools/audit-native-writers.py'))
writer_report=dict(enabled=bool(writer_enabled),cpus=[])
native_pages=[]
if native_mode:
assert param('stub_count')==(native_api.FLAG|(native_api.HVC_FLAG if hvc_mode else 0)|7)
image=xb.kc_load.Image(xb.DEFAULT_KERNELCACHE.read_bytes())
image_pa=param('vbar')-(param('vbar_va')-image.vmin)
admitted=bytearray(image.data)
if hvc_mode:
native_api.patch_hvc_calls(image,admitted,image_pa)
native_contract=native_api.contract(image,admitted,image_pa,hvc=hvc_mode)
native_pages=list(words[8:15])
assert native_pages==native_contract['pages'] and words[15]==0
resident_pages={pa:read(pa,4096) for pa in native_pages}
for va,word in native_contract['call_sites'].items():
pa=image_pa+va-image.vmin
if hvc_mode:word=native_api.HVC_WORD
assert struct.unpack_from('<I',resident_pages[pa&~4095],pa&4095)[0]==word
for va,word in native_contract['writer_sites'].items():
pa=image_pa+va-image.vmin
assert struct.unpack_from('<I',resident_pages[pa&~4095],pa&4095)[0]==word
native_report.update(kernel_sha256=native_contract['kernel_sha256'],
resident_call_and_writer_words_verified=True,
call_pages=[hex(p) for p in native_pages[:5]],
writer_pages=[hex(p) for p in native_pages[5:]],cpus=[])
assert param('write_watch_pa')==0
geometry=dict(root=words[0],frame_map=words[43],frame_low=words[44],frame_high=words[45],
alloc_low=words[46],alloc_high=words[47],aperture_va=words[48],
aperture_size=words[47]-words[30],ram_low=words[30],ram_high=words[31])
# Inspect the uploaded firmware through the resident handoff geometry
# and declared tree length, not a locally regenerated tree. x1 is VA.
bootargs_va=param('x1');handoff_pa=param('watch_pa')
bootargs_pa=handoff_pa+0x1000
assert geometry['ram_low']<=bootargs_pa<geometry['ram_high']-xb.BOOT_ARGS_SIZE
bootargs=read(bootargs_pa,xb.BOOT_ARGS_SIZE)
assert struct.unpack_from('<HH',bootargs)==(xb.BOOT_ARGS_REVISION,xb.BOOT_ARGS_VERSION)
virt_base=struct.unpack_from('<Q',bootargs,8)[0]
assert bootargs_va==virt_base-xb.HANDOFF_SPAN+0x1000
tree_va,tree_len=struct.unpack_from('<QI',bootargs,0x60)
tree_pa=handoff_pa+tree_va-(virt_base-xb.HANDOFF_SPAN)
tree_offset=0x18000 if xb.GUEST_PROFILE.boot_framebuffer_aux else 0x10000
assert tree_pa==handoff_pa+tree_offset and 0<tree_len<=xb.HANDOFF_SPAN-tree_offset
assert geometry['ram_low']<=tree_pa<tree_pa+tree_len<=geometry['ram_high']
tree_blob=read(tree_pa,tree_len);tree=DeviceTree(tree_blob)
if txm_native_report['enabled']:
assert native_params[3]==tree_len
assert read(native_params[1]+0x200000,tree_len)==tree_blob
assert hashlib.sha256(tree_blob).hexdigest()==native_record['tree_sha256']
assert tree.serialize()==tree_blob
if xb.GUEST_PROFILE.boot_framebuffer_aux:
import bootfb
assert tree.get(bootfb.NODE,'compatible')==bootfb.MATCH.encode()+b'\0'
if xb.GUEST_PROFILE.boot_framebuffer_nub:
assert tree.get(bootfb.NODE,'AAPL,boot-display') is None
assert tree.get(bootfb.NUB_NODE,'compatible')==bootfb.NUB_MATCH.encode()+b'\0'
assert tree.get(bootfb.NUB_NODE,'AAPL,boot-display')==b''
assert tree.children[bootfb.NUB_PARENT].count(bootfb.NUB_NODE)==1
else:
assert tree.get(bootfb.NODE,'AAPL,boot-display')==b''
assert bootfb.NUB_NODE not in tree.nodes
if not xb.GUEST_PROFILE.aux_collection_linked:
image=xb.kc_load.Image(xb.DEFAULT_KERNELCACHE.read_bytes())
assert hashlib.sha256(image.data).hexdigest()==bootfb.KERNEL_SHA256
values=struct.unpack('<3Q',read(handoff_pa+bootfb.HANDOFF_BASE,24))
assert values==bootfb.handoff_values(virt_base-xb.HANDOFF_SPAN,image.vmax)
linked_auxkc=None
if xb.GUEST_PROFILE.aux_collection_linked:
# With or without the boot framebuffer bundle: one handoff slot.
import auxkc as auxkc_format
import bootfb
image=xb.kc_load.Image(xb.DEFAULT_KERNELCACHE.read_bytes())
assert hashlib.sha256(image.data).hexdigest()==bootfb.KERNEL_SHA256
values=struct.unpack('<3Q',read(handoff_pa+bootfb.HANDOFF_BASE,24))
collection,admission=xb.linked_auxkc(image,args.auxkc)
# The VA placement and handoff words are a pure function of the
# pinned collection; the PA comes from the resident params (the
# run's place() moved the layout) and must be loader-owned memory.
aux_base=image.vmax;span=(collection.top+0x3fff)&~0x3fff
aux_va_base,aux_va_end,aux_pa,aux_pa_end=aux_range()
assert values==auxkc_format.handoff_values(collection,aux_base,image.vmax)
assert (aux_va_base,aux_va_end,aux_pa_end)==(aux_base,aux_base+span,aux_pa+span)
assert not aux_pa&0x3fff and geometry['ram_low']<=aux_pa<aux_pa+span<=geometry['alloc_low']
assert not (aux_pa<tree_pa+tree_len and aux_pa+span>tree_pa)
linked_auxkc=dict(admission=admission,resident=runpy.run_path(
str(ROOT/'tools/auxkc-state.py'))['resident'](read,collection,aux_base,aux_pa,
image.vmin,image.vmax))
deferred_roots=[s.node for s in xb.GUEST_PROFILE.deferred_subtrees]
published_deferred=sorted(n for n in tree.nodes if any(
n==root or n.startswith(root+'/') for root in deferred_roots))
published_functions=[(s.node,s.name) for s in xb.GUEST_PROFILE.deferred_functions
if tree.get(s.node,s.name) is not None]
if args.expect_deferred:
assert not published_deferred
assert not published_functions
expected=xb.build_devicetree()
assert set(expected.nodes)<=set(tree.nodes)
cmdline=bootargs[0x6c:0x478].split(b'\0',1)[0].decode('ascii')
disk_property=tree.get('/device-tree/chosen/memory-map','RAMDisk')
installed=xb.GUEST_PROFILE.installed_root
spec=installed or xb.GUEST_PROFILE.root_hash
extent_property=tree.get('/device-tree/chosen','boot-ramdmg-extents')
if installed is not None:
assert not args.expect_ramdisk and not args.expect_isc, 'installed boot has no RAM media'
assert disk_property is None and extent_property is None, 'installed boot published RAM media'
assert tree.get('/device-tree/chosen','boot-uuid')==installed.volume_group_uuid.encode('ascii')+b'\0'
assert tree.get('/device-tree/chosen','apfs-preboot-uuid')==installed.volume_group_uuid.encode('ascii')+b'\0'
assert tree.get('/device-tree/chosen','root-snapshot-name')==installed.root_snapshot_name.encode('ascii')+b'\0'
assert not any(w.startswith(('rd=','rootdev=','boot-uuid=')) for w in cmdline.split())
elif args.ramdisk_mode=='iokit':
assert disk_property is None, 'IOKit image must not also publish an md0 device'
disk_property=extent_property
assert disk_property is not None and len(disk_property)==16
assert spec is not None and spec.boot_uuid
assert tree.get('/device-tree/chosen','boot-uuid')==spec.boot_uuid.encode('ascii')+b'\0'
assert tree.get('/device-tree/chosen','apfs-preboot-uuid')==spec.boot_uuid.encode('ascii')+b'\0'
assert tree.get('/device-tree/chosen','boot-ramdmg-size')==disk_property[8:]
assert not any(w.startswith(('rd=','rootdev=','boot-uuid=')) for w in cmdline.split())
else:
assert extent_property is None, 'unexpected IOKit RAM image in md0 capture'
if args.expect_ramdisk:
assert disk_property is not None, 'resident RAMDisk property missing'
ramdisk=None
if disk_property is not None:
assert len(disk_property)==16, 'invalid RAMDisk descriptor size'
disk_pa,disk_size=struct.unpack('<QQ',disk_property)
assert not (disk_pa|disk_size)&0x3fff and disk_size>=0x4000
assert geometry['ram_low']<=disk_pa<disk_pa+disk_size<=geometry['alloc_low']
assert disk_pa>=tree_pa+tree_len
assert struct.unpack_from('<Q',bootargs,32)[0]==geometry['alloc_low']
if args.ramdisk_mode=='md0':
assert [w for w in cmdline.split() if w.startswith(('rd=','rootdev='))]==['rd=md0']
header=read(disk_pa,48)
media_layout=None
root_pa,root_size=disk_pa,disk_size
if header[32:36]!=b'NXSB':
assert args.ramdisk_mode=='iokit', 'GPT RAM disk requires IOKit mode'
import bootmedia
media_layout=bootmedia.inspect(lambda off,size: read(disk_pa+off,size),disk_size)
root_offset,root_size=media_layout['root']
root_pa=disk_pa+root_offset
isc_offset,isc_size=media_layout['isc']
isc_header=read(disk_pa+isc_offset,48)
assert isc_header[32:36]==b'NXSB', 'ISC partition is not raw APFS'
assert struct.unpack_from('<IQ',isc_header,36)==(4096,isc_size//4096)
header=read(root_pa,48)
assert header[32:36]==b'NXSB', 'resident root partition is not raw APFS'
block_size,blocks=struct.unpack_from('<IQ',header,36)
assert block_size==4096 and block_size*blocks==root_size
ramdisk=dict(mode=args.ramdisk_mode,pa=hex(disk_pa),bytes=disk_size,end=hex(disk_pa+disk_size),
block_size=block_size,blocks=blocks,header_hex=header.hex(),
root_pa=hex(root_pa),root_bytes=root_size,gpt=media_layout,
reserved_before_allocator=True,full_payload_verified=False)
auth_blob=tree.get(ROOT_HASH_NODE,spec.property_name) if spec is not None else None
if args.expect_isc:
assert ramdisk is not None and ramdisk.get('gpt') is not None, 'ISC GPT layout missing'
if args.expect_root_hash:
assert auth_blob is not None, 'resident root-hash property missing'
root_hash=None
if auth_blob is not None:
assert ramdisk is not None or installed is not None, 'root-hash capture requires a bound root'
assert len(auth_blob)==spec.payload_bytes
digest=hashlib.sha256(auth_blob).hexdigest()
assert digest==spec.payload_sha256, 'resident root-hash payload differs from profile'
root_hash=dict(node=ROOT_HASH_NODE,property=spec.property_name,
bytes=len(auth_blob),sha256=digest,
expected_root_image_sha256=spec.image_sha256 if installed is None else None,
root_source='installed-ssd' if installed is not None else 'ramdisk',
root_image_readback_verified=False)
if spec.no_protected_data_recipe is not None:
assert tree.get('/device-tree/chosen','protected-data-access')==bytes(4), \
'ephemeral BaseSystem incorrectly advertises protected-data access'
root_hash['protected_data_access']=False
if spec.no_sep_firmware_recipe is not None:
assert tree.get('/device-tree/chosen','sepfw-load-at-boot')==bytes(4), \
'ephemeral BaseSystem incorrectly requests SEP firmware loading'
root_hash['sepfw_load_at_boot']=False
sep_preload=None
sep_contract=(param('sep_firmware_pa'),param('sep_firmware_size'))
geometry['sep_firmware']=sep_contract
# The monitor's rule (probes/xnuboot.c sep_firmware_released): once
# the SEP ROM model took the bootstrap handoff, XNU may free the
# SEPFW extent to the VM, and its bytes are no longer the payload.
sep_released=False
for index in range(words[90]):
model=words[91+index*8:99+index*8]
if model[0]==45 and model[7] and sep_contract[1]:
address=work+offset('context','model_state')+index*13000
sep_released=struct.unpack('<Q',read(address+offset('device_sep_asc','service_ready'),8))[0]!=0
geometry['sep_released']=sep_released
sep_spec=xb.GUEST_PROFILE.sep_preload
sep_descriptor=tree.get('/device-tree/chosen/memory-map','SEPFW')
if sep_spec is not None:
assert tree.get(sep_spec.node,'sepfw-loaded')==b''
assert sep_descriptor is not None and len(sep_descriptor)==16
sep_pa,sep_size=struct.unpack('<QQ',sep_descriptor)
assert sep_contract==(sep_pa,sep_size), 'SEP DMA resource differs from resident preload'
assert not (sep_pa|sep_size)&0x3fff and sep_size==sep_spec.size
assert geometry['ram_low']<=sep_pa<sep_pa+sep_size<=geometry['alloc_low']
if ramdisk is not None:
assert sep_pa+sep_size<=int(ramdisk['pa'],16)
sep_digest=hashlib.sha256(read(sep_pa,sep_size)).hexdigest()
assert sep_released or sep_digest==sep_spec.sha256, 'resident SEP payload differs from profile'
assert all(tree.get('/device-tree/chosen/memory-map',name) is None
for name in ('SEPPatches','uStuff'))
sep_preload=dict(pa=hex(sep_pa),bytes=sep_size,sha256=sep_digest,
full_payload_verified=sep_digest==sep_spec.sha256,
released_to_xnu=sep_released,firmware_execution_verified=False)
else:
assert sep_descriptor is None, 'unexpected SEP preload in selected profile'
assert sep_contract==(0,0), 'unexpected SEP DMA resource in selected profile'
firmware=dict(bootargs_pa=hex(bootargs_pa),bootargs_va=hex(bootargs_va),
tree_pa=hex(tree_pa),tree_va=hex(tree_va),
tree_bytes=tree_len,tree_sha256=hashlib.sha256(tree_blob).hexdigest(),
node_count=len(tree.nodes),deferred_roots=deferred_roots,
command_line=cmdline,ramdisk=ramdisk,expected_ramdisk=args.expect_ramdisk,
root_hash=root_hash,expected_root_hash=args.expect_root_hash,sep_preload=sep_preload,
withdrawn_functions=[(s.node,s.name) for s in xb.GUEST_PROFILE.deferred_functions],
published_functions=published_functions,
published_deferred=published_deferred,expected_deferred=args.expect_deferred)
slide=offset('params','slide')//8;config=words[slide:slide+30]
assert config[5]<=8
boot=[tuple(config[6+i*3:9+i*3]) for i in range(config[5])]
state=read(work+offset('context','slide'),32+50*24)
count=struct.unpack_from('<Q',state,8)[0];assert count<=50
aliases=[struct.unpack_from('<3Q',state,32+i*24) for i in range(count)] if count else boot
geometry.update(boot_ranges=boot,boot_aliases=aliases,
moved=[r for r in aliases if count and config[3]<=r[0]<config[4]])
managed=refs['ManagedFrames'](**{k:geometry[k] for k in
('frame_map','frame_low','frame_high','alloc_low','alloc_high','boot_ranges','boot_aliases')},
released=sep_contract if sep_released else (0,0))
fm=read(geometry['frame_map'],((geometry['frame_high']-geometry['frame_low'])>>14)*16)
users=[];retained=[];shared=[]
for pa in managed.frames():
i=((pa-geometry['frame_low'])>>14)*16;meta=fm[i:i+16]
if meta[2]==18:users.append(pa)
elif meta[2]==19:shared.append(pa)
elif meta[2]==23 and meta[6:8]==b'\0\0':retained.append(pa)
geometry.update(user_roots=users,detached_commpages=retained,shared_roots=shared)
roots=[];qspi=[];dwi=[];i2c=[];pmp=[];spmi=[];watchdog_smc=[]
sep_aon=[];sep_asc=[];dart_runtime=[];cpu_perf_reads=[];acc_sampler=[];aes_idle=[];aes_software=[];aes_key_disable=[]
ans=[];ans_references=[]
for index in range(words[90]):
model=words[91+index*8:99+index*8]
if model[0]==8:
assert model==(8,0x3082dc000,0x8000,0,0x40,0x44,0x48,0)
address=work+offset('context','model_state')+index*13000+offset('device_state','aes_key_disable_writes',nested=True)
writes=struct.unpack('<4I',read(address,16))
aes_key_disable.append(dict(index=index,writes=list(writes),hardware_keys_available=False))
if model[0]==50:
# Virtual ANS2 (docs/ANS2-M2-20260925.md): the reservation sits
# directly above the work arena; its state page is read only.
import ansstate
spec=xb.GUEST_PROFILE.ans_boot
assert spec is not None
from answindow import WINDOW_MODE
assert model==(50,0x38dcc0000,0x60000,end,spec.disk_bytes,1039,
WINDOW_MODE if spec.window else 0,0)
# The IOP firmware state the guest was handed, read back from its
# own device tree (docs/ANS2-FIRMWARE-STATE-20260925.md). Recorded,
# not asserted: a mismatch must not cost the rest of the collection.
from devicemodel import ANS_FIRMWARE_PROPERTIES,ans_firmware_state
nub=spec.node+'/iop-ans-nub'
published=tuple((name,tree.get(nub,name)) for name in ANS_FIRMWARE_PROPERTIES)
# With SNAP_ANS_ASYNC the async block follows (agents/nvme-async); without it the extra
# bytes carry no magic and decode as before.
raw_state=read(end,ansstate.SIZE+ansstate.ASYNC_SIZE)
# The DMA references the model holds (queue bases, mapped command ids,
# SART regions) are SPTM references like the DART's: the ownership audit
# below counts both (docs/ANS2-QUEUES-20260925.md).
ans_references=ansstate.references(raw_state)
physical_nvme = None
if spec.window:
from answindow import WINDOW_NVMEH, decode_nvmeh
physical_nvme = decode_nvmeh(read(end+WINDOW_NVMEH,0x120))
launched = json.loads((ROOT/'build'/f'{args.prefix}-launch.json').read_text())
adopted = json.loads((ROOT/'build'/f'{args.prefix}-nvme-adopt.json').read_text())
assert physical_nvme['writable'] == ('--ans-writable' in launched['arguments'])
assert physical_nvme['params_hex'] == adopted['guest_params_hex']
ans.append(dict(index=index,reservation=hex(end),disk_bytes=model[4],
backend=('ssd-window-readwrite' if physical_nvme['writable'] else
'ssd-window-readonly') if spec.window else 'ramdisk',
physical_nvme=physical_nvme,
state=ansstate.decode(raw_state),dma_references=ans_references,
firmware_state={name:None if value is None else value.hex()
for name,value in published},
firmware_state_published=published==ans_firmware_state(end)))
if model[0]==47:
assert model==(47,0x38500c000,0x4000,5,42,1107,21,1)
address=work+offset('context','model_state')+index*13000+offset('device_state','aes',nested=True)
# Never collect software key bytes, expanded round keys or IVs.
ready,running,mask,threshold,irq,tag=struct.unpack('<6I',read(address,24))
header=struct.unpack('<I',read(address+offset('aes_engine','header'),4))[0]
used,need=struct.unpack('<2I',read(address+offset('aes_engine','used'),8))
commands,transfers,nbytes,barriers,exports=struct.unpack('<5Q',read(address+offset('aes_engine','commands'),40))
assert ready==1 and running<=1 and not mask&~0x3efff and not irq&~0x20
assert threshold in (0,64) and tag<=255 and used<=7 and need<=8
assert not need or used<need
pmgr=[i for i in range(words[90])
if words[91+i*8:95+i*8]==(18,0x300700000,0x11000,0)]
assert len(pmgr)==1
gate=struct.unpack('<I',read(work+offset('context','model_state')+
pmgr[0]*13000+offset('device_state','pmgr_aes',nested=True),4))[0]
assert gate&~0x300 in (0,4,15)
assert gate&15==15 or not (running or mask or irq or header or used or need)
aes_software.append(dict(index=index,ready=ready,running=running,
irq_enable=hex(mask),irq_status=hex(irq),fifo_threshold=threshold,
completed_tag=tag,pending_header=hex(header),pending_used=used,pending_need=need,
commands=commands,transfers=transfers,bytes=nbytes,barriers=barriers,iv_exports=exports,
command_backend=True,dma_backend=True,power_gate=hex(gate)))
if model[0]==46:
assert model==(46,0x38500c000,0x4000,5,0,0,0,0)
address=work+offset('context','model_state')+index*13000
raw=struct.unpack('<8I',read(address+offset('device_state','aes_idle',nested=True),32))
ready,running,mask,threshold,starts,stops,reads,writes=raw
assert ready==1 and running<=1 and not mask&~0x3effe
assert threshold in (0,0x40) and not reads&~0x248 and not writes&~0x1c4
aes_idle.append(dict(index=index,ready=ready,running=running,
irq_enable=hex(mask),fifo_threshold=threshold,starts=starts,stops=stops,
reads=hex(reads),writes=hex(writes),command_backend=False,dma_backend=False))
if model[0]==20:
assert model[1] in (0x210e78000,0x211e78000)
assert model[2:]==(0x1000,int(model[1]==0x211e78000),0,0,0,0)
data=read(work+offset('context','model_state')+index*13000+
offset('device_state','cpu_perf_reads',nested=True),128)
for row in range(2):
raw=struct.unpack_from('<8Q',data,row*64)
if not raw[0]:
assert not any(raw)
continue
assert raw[2]==model[1]+0x48+row*8 and raw[7]<6
cpu_perf_reads.append(dict(index=index,offset=hex(0x48+row*8),
count=raw[0],pc=hex(raw[1]),ipa=hex(raw[2]),esr=hex(raw[3]),
supplied=hex(raw[4]),restored=hex(raw[5]),lr=hex(raw[6]),cpu=raw[7]))
if model[0]==18 and model[1]==0x300700000:
assert model==(18,0x300700000,0x11000,0,1,0,0,0)
level,=struct.unpack('<I',read(work+offset('context','model_state')+
index*13000+offset('device_state','pmgr_sep_aon',nested=True),4))
sep_aon.append(dict(index=index,state=hex(level),level=level&15))
if model[0]==44:
assert model==(44,0x3082b8000,0x1000,8,0,0,0,0)
disarms,=struct.unpack('<I',read(work+offset('context','model_state')+
index*13000+offset('device_state','watchdog_smc_disarms',nested=True),4))
watchdog_smc.append(dict(index=index,base=hex(model[1]),disarms=disarms))
if model[0]==37:
assert model==(37,0x210e44000+model[3]*0x1000000,0x1000,model[3],0,0,0,0)
assert model[3] in (0,1)
address=work+offset('context','model_state')+index*13000
restored,=struct.unpack('<Q',read(address+offset('device_state','acc_boot_policy',nested=True),8))
selectors=struct.unpack('<10Q',read(address+offset('device_state','acc_selectors',nested=True),80))
written,=struct.unpack('<Q',read(address+offset('device_state','acc_selector_written',nested=True),8))
expected=(0x8a0f,0x800f,0x8c0f,0x9c0f,0x9e0f,0x810f,0xb90f,0xbe0f,0xbf0f,0xd40f)
assert restored<=1 and written&~0x3ff==0
assert restored or not written
assert all(v in (0,expected[i]) and (written&(1<<i) or not v)
for i,v in enumerate(selectors))
acc_sampler.append(dict(index=index,base=hex(model[1]),cluster=model[3],
restored=restored,written=hex(written),
selectors={hex(0x18+i*16):hex(v) for i,v in enumerate(selectors)},
counter_producer=False))
if model[0]==45:
assert model in ((45,0x282600000,0x9000,8,0,0,0,0),
(45,0x282600000,0x9000,8,1,346,343,0),
(45,0x282600000,0x9000,8,1,346,343,1),
(45,0x282600000,0x9000,8,1,346,343,3),
(45,0x282600000,0x9000,8,1,346,343,7),
(45,0x282600000,0x9000,8,1,346,343,11),
(45,0x282600000,0x9000,8,1,346,343,15),
(45,0x282600000,0x9000,8,1,346,343,19),
(45,0x282600000,0x9000,8,1,346,343,27),
(45,0x282600000,0x9000,8,1,346,343,59))
reply_transport=bool(model[4])
address=work+offset('context','model_state')+index*13000
data=read(address,offset('device_sep_asc','xars')+16*8)
def asc_field(name,fmt):
return struct.unpack_from(fmt,data,offset('device_sep_asc',name))
ready,=asc_field('ready','<I');enable=asc_field('enable','<2I')
count,=asc_field('count','<I');staged_valid,=asc_field('staged_valid','<I')
staged,=asc_field('staged','<Q');submitted,=asc_field('submitted','<Q')
requests=asc_field('request','<16Q')
replies=asc_field('reply','<16Q')
consumed,=asc_field('consumed','<Q')
published,=asc_field('published','<Q')
delivered,=asc_field('delivered','<Q')
reply_count,=asc_field('reply_count','<I')
status_replies,=asc_field('rom_status_replies','<Q')
last_request,=asc_field('rom_last_request','<Q')
last_reply,=asc_field('rom_last_reply','<Q')
rom_buffers={k:asc_field(k,'<Q')[0] for k in
('rom_stage','rom_firmware_iova','rom_firmware_size','rom_shared_iova','rom_shared_size')}
service={k:asc_field(k,'<Q')[0] for k in ('service_ready','service_notifications')}
service['service_resources']=list(asc_field('bootstrap','<8I'))
service['entropy_words']=asc_field('entropy_words','<Q')[0]
service['entropy_last_request']=hex(asc_field('entropy_last_request','<Q')[0])
service['status_queries']=asc_field('status_queries','<Q')[0]
service['status_last_request']=hex(asc_field('status_last_request','<Q')[0])
service['virtual_effective_status']=0 if model[7]&12 else None
pnon_words=asc_field('pnon','<26Q')
service['pnon']=dict(zip(('ready','advertisements','negotiations','queries','last_request','last_reply'),pnon_words[:6]))
service['pnon'].update(stage=list(pnon_words[6:8]),size=list(pnon_words[8:10]),
iova=list(pnon_words[10:12]),physical=[list(pnon_words[12:19]),list(pnon_words[19:26])])
xars_words=asc_field('xars','<16Q')
service['xars']=dict(zip(('ready','advertisements','negotiations','queries','last_request','last_reply'),xars_words[:6]))
service['xars'].update(stage=list(xars_words[6:8]),size=list(xars_words[8:10]),
iova=list(xars_words[10:12]),physical=[list(xars_words[12:14]),list(xars_words[14:16])])
if model[7]&32:
assert offset('sep_sks_endpoint','wire')==128
assert offset('sep_sks_wire_state','environment')==12
service['sks']=decode_sks(read(address+offset('device_sep_asc','sks'),SKS_STATE_BYTES))
assert ready==1 and all(e<=1 for e in enable) and count<=8 and staged_valid<=1
assert submitted>=consumed and submitted-consumed==count and not any(requests[count*2:])
assert reply_count<=8 and published>=delivered and published-delivered==reply_count
assert not any(replies[reply_count*2:])
assert reply_transport or not (consumed or published or delivered or reply_count)
assert staged_valid or not staged
assert not staged_valid or count<8
sep_asc.append(dict(index=index,base=hex(model[1]),ready=ready,
enable=list(enable),count=count,submitted=submitted,
consumed=consumed,published=published,delivered=delivered,reply_count=reply_count,
reply_transport=reply_transport,
rom_status_producer=bool(model[7]),rom_status_replies=status_replies,
rom_last_request=hex(last_request),rom_last_reply=hex(last_reply),**rom_buffers,**service,
service_bootstrap=bool(model[7]&2),
pnon_recovery=bool(model[7]&4),
xars_refusal=bool(model[7]&8),
pnon_refusal=bool(model[7]&16),
sks_startup=bool(model[7]&32),
staged=hex(staged),staged_valid=staged_valid,
control_reads=list(asc_field('reads','<2Q')),
control_writes=list(asc_field('writes','<2Q')),
requests=[[hex(requests[2*i]),hex(requests[2*i+1])] for i in range(count)],
replies=[[hex(replies[2*i]),hex(replies[2*i+1])] for i in range(reply_count)],
receive_producer=bool(published),firmware_running=False))
audit_rom_history(sep_asc[-1])
# Validate raw wire shape, then discard queued random DATA from
# the serialized audit. Counts/tags suffice for reconstruction.
for pair in sep_asc[-1]['replies']:
if int(pair[0],16)&0xffff00ff==0x360000:
pair[0]=hex(int(pair[0],16)&0xffffffff)
sep_asc[-1]['entropy_payload_redacted']=True
if model[0] in (9,48):
raw=struct.unpack('<283I',read(work+offset('context','model_state')+
index*13000+offset('device_state','spmi'),1132))
head,count,tx_words,tx_need=raw[279:283]
assert head<256 and count<=256 and tx_words<=tx_need<=5
recovery=struct.unpack('<3I',read(work+offset('context','model_state')+
index*13000+offset('device_state','spmi_recovery',nested=True),12))
spmi.append(dict(index=index,base=hex(model[1]),
command=hex(raw[274]),payload=list(raw[275:279]),
rx_head=head,rx_count=count,tx_words=tx_words,tx_need=tx_need,
reply=[raw[(head+i)%256] for i in range(count)],
enable=list(raw[256:265]),flags=list(raw[265:274]),
recovery=dict(zip(('config_reads','config_writes','fifo_clears'),recovery))))
if model[0]==48:
assert model[:6]==(48,0x308700000,0x18000,0x14000,0x4000,487)
assert model[7]==0xf715f704f802000e
seed=json.loads((ROOT/'build'/f'{args.prefix}-rtc-seed.json').read_text())
assert model[6]==seed['epoch_us'] and seed['after_upload'] is True
assert hashlib.sha256(struct.pack(f'<{len(words)}Q',*words)).hexdigest()==seed['parameter_sha256']
address=work+offset('context','model_state')+index*13000+offset('device_state','pmu_rtc',nested=True)
rtc_raw=read(address,112)
header=struct.unpack_from('<4I',rtc_raw)
names=('epoch_us','start','frequency','last_now','last_ticks',
'counter_reads','offset_reads','offset_writes','rejected','last_command','last_length')
rtc=dict(zip(names,struct.unpack_from('<11Q',rtc_raw,16)))
assert header[:3]==recovery and header[3]==1
assert rtc['epoch_us']==seed['epoch_us'] and rtc['frequency']==param('clock_guest_hz')
assert rtc['last_now']>=rtc['start'] and rtc['last_length']<=8
if rtc['counter_reads']:
us=rtc['epoch_us'];delta=rtc['last_now']-rtc['start'];hz=rtc['frequency']
expected_ticks=(us//1000000)*32768+(us%1000000)*32768//1000000
expected_ticks+=(delta//hz)*32768+(delta%hz)*32768//hz
assert rtc['last_ticks']==expected_ticks<1<<47
else:assert rtc['last_ticks']==0
rtc['offset_hex']=rtc_raw[104:110].hex()
rtc['seed_sha256']=hashlib.sha256((ROOT/'build'/f'{args.prefix}-rtc-seed.json').read_bytes()).hexdigest()
rtc['boundary']='Virtual UTC counter plus volatile guest-written offset; no PMU power or alarm service'
spmi[-1]['rtc']=rtc
if model[0]==25:
assert model==(25,0x3003c0000,0x14000,0x10000,0x1a8,0x1b8,8,0xffb80)
raw=struct.unpack('<2Q5I',read(work+offset('context','model_state')+index*13000,36))
assert raw[4]==1 and all(v<=1 for v in raw[2:6])
pmp.append(dict(index=index,request=raw[0],ack=raw[1],
request_updated=raw[2],ack_updated=raw[3],ready=raw[4],
ready_updated=raw[5],status_reads=raw[6]))
if model[0]==43:
assert model==(43,0x385018000,0x4000,2,6,1110,0,0)
raw=struct.unpack('<32I',read(work+offset('context','model_state')+index*13000,128))
assert raw[22]<=16 and raw[23]<=1 and not raw[24]&~0x08200000 and raw[25]==1 and raw[31]<=1
i2c.append(dict(index=index,control=raw[0],mask=raw[1],address=raw[2],
timing=list(raw[3:6]),tx_words=list(raw[6:22]),tx_count=raw[22],
blocked=raw[23],status=raw[24],ready=raw[25],resets=raw[26],
nacks=raw[27],last_address=raw[28],shadow_command=raw[29],shadow_writes=raw[30],halt=raw[31]))
if model[0]==42:
assert model==(42,0x300200000,0x4000,0,0,0,0,0)
raw=struct.unpack('<7I',read(work+offset('context','model_state')+index*13000,28))
expected=(1,192000,120,0,0x41)
assert raw[6]==1 and not raw[5]&~31
assert all(v==(expected[i] if raw[5]&(1<<i) else 0) for i,v in enumerate(raw[:5]))
dwi.append(dict(index=index,ready=raw[6],written_mask=raw[5],values=list(raw[:5])))
if model[0]==41:
assert model==(41,0x385110000,0x5000,0,0,0,0,0)
raw=struct.unpack('<36I',read(work+offset('context','model_state')+index*13000,144))
# This revision has no admitted transfer producer. All real
# queues remain empty; a reset is counted only on a served
# guest reset write, not merely on device initialization.
assert not any(raw[:34]) and raw[34]==1
qspi.append(dict(index=index,ready=raw[34],resets=raw[35],
tx_words=raw[32],rx_words=raw[33]))
if model[0]!=17:continue
state=read(work+offset('context','model_state')+index*13000,2184)
initialized,powered=struct.unpack_from('<2I',state)
assert not initialized&~model[7] and not powered&~initialized
enabled=struct.unpack_from('<8I',state,offset('dart_state','enabled'))
tcr=struct.unpack_from('<256I',state,offset('dart_state','tcr'))
ttbr=struct.unpack_from('<256I',state,offset('dart_state','ttbr'))
dart_runtime.append(dict(index=index,base=hex(model[1]),
policy=model[4],streams=model[3],enabled=[hex(v) for v in enabled],
tcr={str(i):hex(v) for i,v in enumerate(tcr) if v},
ttbr={str(i):hex(v) for i,v in enumerate(ttbr) if v},
initialized_ids=hex(initialized),powered_ids=hex(powered),
runtime_zero=not any(state[8:]),state_sha256=hashlib.sha256(state).hexdigest()))
if not (initialized or powered):continue
if model[4] not in (300,556,1068):continue # SIO, normal SEP and Adreno
assert initialized==powered and initialized.bit_count()==1
if model[4]==1068:
assert model[1]==0x710000000 and model[3]==16 and initialized==1<<31
ttbr=struct.unpack_from('<256I',state,1064)
roots.extend((initialized.bit_length()-1,sid,t,tcr[sid])
for sid,t in enumerate(ttbr) if t)
dart=darts['audit'](read,roots=roots,**{k:geometry[k] for k in
('frame_map','frame_low','frame_high','alloc_low','alloc_high','boot_ranges','boot_aliases',
'sep_firmware','sep_released')})
for asc in sep_asc:
asc['rom_buffer_audit']=audit_rom_buffers(asc,dart['leaves'],geometry)
asc['service_resource_audit']=audit_bootstrap(asc,dart['leaves'],read)
asc['pnon_buffer_audit']=audit_pnon_buffers(asc,dart['leaves'],geometry,read)
asc['xars_buffer_audit']=audit_xars_buffers(asc,dart['leaves'],geometry)
user_state=work+offset('context','user_roots')
active_roots=list(struct.unpack('<6Q',read(user_state,48)))
cached=[];trees={}
for index in range(64):
values=struct.unpack('<7Q',read(user_state+48+index*56,56))
entry=dict(zip(('root','shadow','leaves','asid','format','flags','valid'),values))
entry['index']=index
assert entry['valid'] in (0,1)
if entry['root']:cached.append(entry)
# Every slot that still has a tree (valid or not) may link groups.
if entry['shadow']:trees[index]=entry['shadow']
root_counts=struct.unpack('<5Q',read(user_state+3632,40))
assert root_counts[0]==root_counts[1]+root_counts[2]
assert not q(work+offset('context','root_install_failures'))
root_auditor=runpy.run_path(str(ROOT/'tools/audit-user-roots.py'))
sprr_fields=('uperm','pperm','umprr','pmprr','resets','reads','writes','rejected','last_reg','last_value')
sprr_states=[dict(zip(sprr_fields,struct.unpack('<10Q',read(user_state+3672+80*cpu,80))))
for cpu in range(6)]
assert sum(s['resets'] for s in sprr_states)==root_counts[0]+6
loaded_roots=[dict(zip(('root','flags','resets'),values)) for values in
struct.iter_unpack('<3Q',read(work+offset('context','root_loaded'),144))]
private_audit=runpy.run_path(str(ROOT/'tools/audit-user-permissions.py'))
view_states=[dict(zip(private_audit['STATE_FIELDS'],values)) for values in
struct.iter_unpack('<9Q',read(work+offset('context','user_view_state'),432))]
assert not q(work+offset('context','user_view_failed'))
# Native page tables (SNAP_NATIVE_TABLES). The context area exists in
# every build: a monitor that runs user roots on shared permission
# variants counts each install, any other leaves the area all zero.
native_fields=runpy.run_path(str(ROOT/'tools/kernel_ops_perf.py'))['RECORDS']['native_stats'][1]
native_raw=read(work+offset('context','native_stats'),8*len(native_fields))
native_stats=dict(zip(native_fields,struct.unpack(f'<{len(native_fields)}Q',native_raw)))
cpu_bytes=private_audit['NATIVE_CPU_BYTES']
native_cpu_raw=read(work+offset('context','native_cpu'),6*cpu_bytes)
native_cpus=[private_audit['native_cpu'](native_cpu_raw[n*cpu_bytes:(n+1)*cpu_bytes]) for n in range(6)]
sources_start=offset('context','native_sources')
native_sources_raw=read(work+sources_start,offset('context','native_stats')-sources_start)
native_tables=native_stats['installs']>0
if not native_tables:
assert not any(native_raw+native_cpu_raw+native_sources_raw),'native table state without native installs'
else:
# Every page whose stores reach variant copies is a retained source
# of some slot's views, and every such source is marked.
arena_base=q(work+offset('context','work_base'))
marked={arena_base+4096*(8*i+b) for i,byte in enumerate(native_sources_raw)
for b in range(8) if byte>>b&1}
sources=set()
for view in range(64):
base=work+offset('context','user_views')+8256*view
count=struct.unpack('<Q',read(base+8,8))[0]
assert count<=128,'invalid retained view count'
if count:sources.update(r[0] for r in struct.iter_unpack('<8Q',read(base+64,64*count)))
assert marked==sources,'native source bitmap differs from retained variant sources'
assert not native_stats['refused'] and not native_stats['audit_failures'] and not native_stats['reuse_failures']
# Shared-region groups (SNAP_NATIVE_SHARED; the area exists in every
# build and is all zero without it): exact translations of their guest
# tables, the whole allocation domain, links as recorded.
shared_block=next(b for b in layout.split('*** Dumping AST Record Layout')
if re.search(r'^\s*0 \| struct native_shared\n',b))
shared_raw=read(work+offset('context','native_shared'),
int(re.search(r'\[sizeof=(\d+),',shared_block)[1]))
native_shared=dict(enabled=False,pages=[])
if any(shared_raw):
assert native_tables,'shared groups without native page tables'
shared_audit=runpy.run_path(str(ROOT/'tools/native_shared_audit.py'))
pool=work+offset('context','table_pool')
capacity=offset('table_pool','next')-offset('table_pool','owner')
arena_base=q(work+offset('context','work_base'))
used=(q(work+offset('context','next_table'))-arena_base)//4096
owners=read(pool+offset('table_pool','owner'),capacity)[:used]
summary=shared_audit['audit'](read,shared_raw,owners,arena_base,trees,
group_offset=offset('native_shared','group'),index_offset=offset('native_shared','index'))
native_shared=dict(enabled=True,header=summary['header'],groups=summary['groups'],
leaves=summary['leaves'],links=summary['links'],
pages=sorted(hex(p) for p in summary['pages']))
user_permissions=[];user_storage=set()
for entry in cached:
if not entry['valid']:continue
views=dict(state=read(work+offset('context','user_views')+8256*entry['index'],8256),
entry=entry,active=active_roots,sprr=sprr_states,modes=view_states,loaded=loaded_roots,
frame_map=fm,frame_low=geometry['frame_low'],work_low=work,
work_high=q(work+offset('context','next_table')),
diagnose_mismatches=args.diagnose_user_mapping_mismatches)
audit=(private_audit['audit_native'](read,native=native_cpus,**views) if native_tables else
private_audit['audit'](read,**views))
storage=set(audit.pop('private_storage'))
assert not storage&user_storage;user_storage|=storage
user_permissions.append(audit)
root_switch=root_auditor['audit'](read,cache=cached,active=active_roots,
ram_low=geometry['ram_low'],ram_high=geometry['ram_high'],work_low=work,work_high=table_end,
sprr=sprr_states,translated_tpro=True,loaded=loaded_roots,
diagnose_mismatches=args.diagnose_user_mapping_mismatches,
shared_pages=frozenset(int(p,16) for p in native_shared['pages']))
user_mapping_qualified=(all(a['private_descriptors_verified'] for a in user_permissions)
and root_switch['mappings_equal']
and root_switch['cached_leaf_counts_equal'])
update_auditor=runpy.run_path(str(ROOT/'tools/audit-user-update.py'))
update_states=[dict(zip(update_auditor['FIELDS'],values)) for values in
struct.iter_unpack('<16Q',read(work+offset('context','user_updates'),768))]
user_updates=update_auditor['audit'](read,states=update_states,cache=cached,
ram_low=geometry['ram_low'],ram_high=geometry['ram_high'])
region_states=[dict(zip(update_auditor['FIELDS'],values)) for values in
struct.iter_unpack('<16Q',read(work+offset('context','user_regions'),768))]
user_regions=update_auditor['audit'](read,states=region_states,cache=cached,
ram_low=geometry['ram_low'],ram_high=geometry['ram_high'],region=True)
shared_region_auditor=runpy.run_path(str(ROOT/'tools/audit-shared-region.py'))
shared_region_states=[dict(zip(shared_region_auditor['FIELDS'],values)) for values in
struct.iter_unpack('<16Q',read(work+offset('context','shared_regions_updates'),1536))]
shared_regions_updates=shared_region_auditor['audit'](read,
states=[shared_region_states[i:i+2] for i in range(0,12,2)],
ram_low=geometry['ram_low'],ram_high=geometry['ram_high'],
frame_map=geometry['frame_map'],frame_low=geometry['frame_low'])
disjoint_auditor=runpy.run_path(str(ROOT/'tools/audit-unmap-disjoint.py'))
disjoint_states=[dict(zip(disjoint_auditor['FIELDS'],values)) for values in
struct.iter_unpack('<16Q',read(work+offset('context','user_disjoint_unmaps'),1536))]
user_disjoint_unmaps=disjoint_auditor['audit'](read,
states=[disjoint_states[i:i+2] for i in range(0,12,2)],
ram_low=geometry['ram_low'],ram_high=geometry['ram_high'],
frame_map=geometry['frame_map'],frame_low=geometry['frame_low'])
kernel_disjoint_unmaps=None
if re.search(r' kernel_disjoint_unmaps$',layout,re.M):
kernel_disjoint_states=[dict(zip(disjoint_auditor['FIELDS'],values)) for values in
struct.iter_unpack('<16Q',read(work+offset('context','kernel_disjoint_unmaps'),1536))]
kernel_disjoint_unmaps=disjoint_auditor['audit'](read,
states=[kernel_disjoint_states[i:i+2] for i in range(0,12,2)],
ram_low=geometry['ram_low'],ram_high=geometry['ram_high'],
frame_map=geometry['frame_map'],frame_low=geometry['frame_low'],
kernel_root=geometry['root'])
replacement_auditor=runpy.run_path(str(ROOT/'tools/audit-user-map-replace.py'))
replacement_states=[dict(zip(replacement_auditor['FIELDS'],values)) for values in
struct.iter_unpack('<16Q',read(work+offset('context','user_map_replacements'),768))]
user_map_replacements=replacement_auditor['audit'](read,states=replacement_states,
ram_low=geometry['ram_low'],ram_high=geometry['ram_high'],
frame_map=geometry['frame_map'],frame_low=geometry['frame_low'])
shared_page_auditor=runpy.run_path(str(ROOT/'tools/audit-shared-page.py'))
shared_page_state=dict(zip(shared_page_auditor['FIELDS'],
struct.unpack('<16Q',read(work+offset('context','shared_map_calls'),128))))
root_switch.update(zip(('switches','user_switches','kernel_switches','builds','invalidations'),root_counts))
# Monitors built before cache eviction have no counter; report None.
if re.search(r' root_evictions$',layout,re.M):
root_switch['evictions']=q(work+offset('context','root_evictions'))
assert root_switch['evictions']<=root_switch['invalidations'],'evictions are invalidations'
else:root_switch['evictions']=None
# EL0 crash evidence: the fatal-exception delivery counters and, with
# SNAP_USER_CRASH_RING, the last crash-class events per vCPU. Fields
# an older monitor lacks decode as None.
el0_crashes=runpy.run_path(str(ROOT/'tools/user_crash_ring.py'))['read_context'](
read,work,offset,layout)
# PV self-checks (struct sptm_pv_audit); None for older monitors.
pv_audit=runpy.run_path(str(ROOT/'tools/pv_counters.py'))['read_context'](
read,work,offset,layout)
# Linked-AuxKC MAP_PAGE/UPDATE/RETYPE observation; None when unused.
auxkc_exec=runpy.run_path(str(ROOT/'tools/auxkc_counters.py'))['read_context'](
read,work,offset,layout,lambda name:aux_range())
assert (auxkc_exec is None)==(linked_auxkc is None),'AuxKC counters disagree with the profile'
if linked_auxkc is not None:linked_auxkc['monitor_counters']=auxkc_exec
root_installed=struct.unpack('<12Q',read(work+offset('context','root_installed'),96))
import ansstate
ownership=refs['audit'](read,dma_references=ansstate.merge_references(
dart['data_references'],ans_references),
active_user_roots=[root for root in active_roots if root],**geometry)
nest_audit=runpy.run_path(str(ROOT/'tools/audit-nest-region.py'))
unnest_audit=runpy.run_path(str(ROOT/'tools/audit-unnest-region.py'))
detach_audit=runpy.run_path(str(ROOT/'tools/audit-shared-detach.py'))
shared_detaches=detach_audit['audit_shared_detaches'](read,
read(work+offset('context','shared_detaches'),8+4096*64),
frame_map=geometry['frame_map'],frame_low=geometry['frame_low'])
unnesting=unnest_audit['audit_unnesting'](read,read(work+offset('context','unnest_calls'),368),
frame_map=geometry['frame_map'],frame_low=geometry['frame_low'],
detachments=shared_detaches['records'])
nesting=nest_audit['audit_nesting'](read,read(work+offset('context','nest_state'),64),
frame_map=geometry['frame_map'],frame_low=geometry['frame_low'],ownership=ownership,
removed=unnesting['descriptors'],detachments=shared_detaches['records'])
service=struct.unpack('<8Q',read(work+offset('context','services'),64))
service_budget_stops=list(struct.unpack('<6Q',read(work+offset('context','service_budget_stops'),48)))
assert all(v in (0,1) for v in service_budget_stops) and sum(service_budget_stops)<=1
if any(service_budget_stops):
assert service[1]==0x100000001+service_budget_stops.index(1)
assert service[0]==0 and service[2]==0x8000000000000003 and not any(service[3:])
assert service[1]>>32 in (1,2) and 1<=service[1]&0xffffffff<=6
# Early stop (compat/early-stop.h): why the run ended before its budget,
# when it did. A stop it caused is the normal stop of the CPU that fired.
early_stop=dict(present=False)
if re.search(r'^\s*\d+ \| struct early_stop_state early_stop$',layout,re.M):
names=runpy.run_path(str(ROOT/'tools/early_stop.py'))
early_stop=dict(present=True,**names['decode'](
read(work+offset('context','early_stop'),names['SIZE'])))
assert early_stop['state'] in (0,2,3) and early_stop['outcome'] in (0,1,2,3)
if early_stop['outcome']==1:
assert early_stop['state']==3 and service[1]==(1<<32)|early_stop['fire_cpu']
early_stop['summary']=names['summary'](early_stop,info.get('timer_hz'))
cpu_rows=[];banks=[];runtime_pages=set();canonical_roots=[]
for slot in range(6):
address=work+offset('context','vcpus')+slot*2368;raw=read(address,2368)
def field(name):return struct.unpack_from('<Q',raw,offset('vcpu',name))[0]
assert field('context')==work and field('slot')==slot
canonical_roots.append(field('shadow_root'))
assert field('vmpidr')==0x80000000|(slot if slot<4 else slot-4+0x100)
fault=field('service_fault')
if args.diagnose_primary_timeout and slot==0:
assert fault==6 and service[1]==0x200000001
else:assert fault==0
installed_ttbr,installed_tcr=root_installed[slot*2:slot*2+2]
if active_roots[slot]:
cached_root=next(e for e in cached if e['root']==active_roots[slot] and e['valid'])
assert field('guest_ttbr0')==active_roots[slot]|(cached_root['asid']<<48)
if native_tables:
# Installed for the reset state at EL1 (PPERM governs), in
# this slot's own ASID; later flips are TTBR0 writes that
# audit_native checked against the CPU's SPRR state.
reset_w=(private_audit['PPERM']>>36)&1
assert installed_ttbr==native_cpus[slot]['ttbr0'][reset_w][0]
else:
assert installed_ttbr==view_states[slot]['shadow']|(cached_root['asid']<<48)
elif installed_ttbr:
assert installed_ttbr==field('low_root') and field('guest_ttbr0')==geometry['root']
if native_tables and not active_roots[slot]:
assert not native_cpus[slot]['installed'] and not native_cpus[slot]['errors']
if installed_ttbr:
assert installed_tcr&0x3f==17 and not installed_tcr&(3<<14)
assert (installed_tcr>>30)&3==2 and not installed_tcr&(1<<7)
root=field('stage2_root');n=field('guard_count');assert 0<n<=12
assert field('open_guards')==0
guards=[struct.unpack_from('<4Q',raw,offset('vcpu','guards')+i*32) for i in range(n)]
if native_mode:
expected_guards=set(([] if writer_enabled else native_pages[5:])+[param('vbar')])
expected_guards.update(param('watch_pa')+i*4096 for i in range(param('watch_count')))
assert {g[0] for g in guards}==expected_guards
assert all(g[0] not in native_pages[:5] for g in guards)
state=struct.unpack('<12Q',read(work+offset('context','native_calls')+slot*96,96))
ready,calls,restores,rejected=state[:4]
assert ready==int(field('prepared')==1) and calls==restores and rejected==0
hvc_calls,hvc_rejected,hvc_pc=struct.unpack('<3Q',
read(work+offset('context','sptm_hvc')+slot*24,24))
assert hvc_rejected==0
if hvc_mode:
assert calls==0
if hvc_calls:assert hvc_pc in native_contract['call_sites'] and ready
else:
assert (hvc_calls,hvc_rejected,hvc_pc)==(0,0,0)
if calls:
assert state[9] in native_api.CALL_WORDS and (state[10]&31) in (8,9)
assert state[11]==((state[10]&~12)|4)
assert native_contract['call_sites'].get(state[8])==state[9]
native_report['cpus'].append(dict(slot=slot,ready=ready,calls=calls,
restores=restores,rejected=rejected,last_pc=hex(state[8]),
hvc_calls=hvc_calls,hvc_rejected=hvc_rejected,hvc_last_pc=hex(hvc_pc),
last_word=hex(state[9]),saved_spsr=hex(state[10]),physical_spsr=hex(state[11])))
banks.append(dict(root=root,vttbr=root|((slot+1)<<48),guards=guards))
vncr=field('vncr_va');lo=vncr;hi=vncr+4096 if not slot else field('el2_sp')
assert not (lo|hi)&4095 and work<=lo<hi<=table_end
pages=set(range(lo,hi,4096));assert not runtime_pages&pages;runtime_pages|=pages
seed=bytearray(4096);struct.pack_into('<Q',seed,0x78,xb.GUEST_PROFILE.kernel_hcr)
page=read(vncr,4096);assert struct.unpack_from('<Q',page,0x78)[0]==xb.GUEST_PROFILE.kernel_hcr
active=field('prepared')==1
writer_row=writer_api['audit'](
read(work+offset('context','writer_debug')+slot*writer_api['SIZE'],writer_api['SIZE']),
enabled=bool(writer_enabled),prepared=active)
writer_report['cpus'].append(dict(writer_row,slot=slot))
row=dict(slot=slot,prepared=active,service_acquires=field('service_acquires'),
vncr=hex(vncr),vncr_seed_unchanged=page==seed,closed_guards=n)
ipi=struct.unpack_from('<6Q',raw,offset('vcpu','ipi'))
assert ipi[0]<=1 and ipi[1]==ipi[0]+ipi[3]+ipi[4]
if not active:assert not any(ipi)
row['ipi']=dict(zip(('pending','received','reads','acks','coalesced','last_request'),ipi))
row['ipi'].update(request_pc=hex(field('ipi_request_pc')),ack_pc=hex(field('ipi_ack_pc')))
row['timer_irqs']=field('timer_irqs')
if slot:
out=field('out_ptr');stack=field('el2_sp')-65536
assert out==vncr+4096 and stack==out+4096
stack_bytes=read(stack,65536);output=read(out,4096)
row.update(stack_sha256=hashlib.sha256(stack_bytes).hexdigest(),
output_sha256=hashlib.sha256(output).hexdigest())
if not active:
assert not any(stack_bytes) and not any(output) and page==seed
assert field('service_acquires')==0
else:assert field('service_acquires')>0
cpu_rows.append(row)
kernel_audit=runpy.run_path(str(ROOT/'tools/audit-kernel-permissions.py'))
kernel_permissions=kernel_audit['audit'](read,
state=read(work+offset('context','kernel_views'),8256),
installed=struct.unpack('<6Q',read(work+offset('context','kernel_installed'),48)),
prepared=[row['prepared'] for row in cpu_rows],canonical=canonical_roots,
pperm=[s['pperm'] for s in sprr_states],
work_low=work,work_high=q(work+offset('context','next_table')))
assert not q(work+offset('context','kernel_view_failed'))
kernel_update_states=[dict(zip(update_auditor['FIELDS'],values)) for values in
struct.iter_unpack('<16Q',read(work+offset('context','kernel_updates'),768))]
kernel_updates=update_auditor['audit_kernel'](read,states=kernel_update_states,
root=geometry['root'],canonical=int(kernel_permissions['canonical_root'],16),
ram_low=geometry['ram_low'],ram_high=geometry['ram_high'])
kernel_permissions.update(kernel_audit['audit_aperture'](read,
protected=kernel_permissions.pop('protected'),root=geometry['root'],
readonly=kernel_permissions.pop('readonly'),
txm_apertures=kernel_permissions.pop('txm_apertures'),
frame_map=fm,frame_low=geometry['frame_low'],aperture_low=geometry['ram_low'],
aperture_va=geometry['aperture_va'],aperture_size=geometry['aperture_size'],
moved=geometry['moved']))
for name in ('papt_protects','papt_releases','kernel_permission_changes',
'user_exec_protects','user_exec_releases','user_exec_cache_syncs'):
kernel_permissions[name]=q(work+offset('context',name))
assert kernel_permissions['user_exec_protects']==kernel_permissions['user_exec_cache_syncs']
if re.search(r' txm_donations$',layout,re.M):
kernel_permissions['txm_donations']=q(work+offset('context','txm_donations'))
imports=releases=0
if re.search(r' txm_imports$',layout,re.M):
imports=q(work+offset('context','txm_imports'));releases=q(work+offset('context','txm_releases'))
assert imports==releases
kernel_permissions.update(txm_imports=imports,txm_releases=releases)
assert kernel_permissions['txm_donations']-imports==kernel_permissions['txm_default_frames']
if txm_native_report['enabled']:assert imports==2*txm_native_report['load']['calls']
assert kernel_permissions['user_exec_protects']-kernel_permissions['user_exec_releases']==kernel_permissions['user_exec_frames']
user_execution_audit=runpy.run_path(str(ROOT/'tools/audit-user-execution.py'))
user_execution=user_execution_audit['audit'](
read(work+offset('context','user_execution'),6*23*8))
pan_entry_audit=runpy.run_path(str(ROOT/'tools/audit-pan-entry.py'))
pan_entry=pan_entry_audit['audit'](read(work+offset('context','pan_entry'),6*8*8))
user_clock_audit=runpy.run_path(str(ROOT/'tools/audit-user-clock.py'))
user_clock=user_clock_audit['audit'](read(work+offset('context','user_clock'),6*11*8))
conflict_audit=runpy.run_path(str(ROOT/'tools/audit-user-map-conflict.py'))
user_map_conflict=conflict_audit['audit'](read(work+offset('context','user_map_conflict'),384))
user_sprr_audit=runpy.run_path(str(ROOT/'tools/audit-user-sprr.py'))
user_sprr=user_sprr_audit['audit'](read(work+offset('context','user_sprr'),384))
uperm_audit=runpy.run_path(str(ROOT/'tools/audit-uperm-write.py'))
uperm_write=uperm_audit['audit'](read(work+offset('context','sprr_uperm_update'),672),
translated_tpro=True)
pperm_audit=runpy.run_path(str(ROOT/'tools/audit-pperm-copy.py'))
pperm_copy=pperm_audit['audit'](read(work+offset('context','sprr_pperm_update'),720),
[s['pperm'] for s in sprr_states],translated_tpro=True)
tpro_protects=q(work+offset('context','user_tpro_protects'))
tpro_releases=q(work+offset('context','user_tpro_releases'))
tpro_live=sum(fm[off+2]==17 for off in range(0,len(fm),16))
assert tpro_protects-tpro_releases==tpro_live
const_begin,const_end=param('const_begin'),param('const_end')
image=xb.kc_load.Image(Path(xb.DEFAULT_KERNELCACHE).read_bytes())
expected=xb.const_region_contract(image,const_begin)
assert expected==(const_begin,const_end)
assert const_begin==struct.unpack_from('<Q',bootargs,16)[0]
assert param('mem_low')<=const_begin<const_end<=param('alloc_low')
sealed=q(work+offset('context','const_sealed_cpus'))
lockdowns=q(work+offset('context','lockdowns'))
invalidations=q(work+offset('context','const_tlb_invalidations'))
reads=struct.unpack('<2Q',read(work+offset('context','const_reads'),16))
assert sealed in (0,63) and bool(sealed)==bool(lockdowns)
assert invalidations==lockdowns*6
table_start=q(work+offset('context','work_base'))
context_block=next(b for b in layout.split('*** Dumping AST Record Layout')
if re.search(r'^\s*0 \| struct context\n',b))
context_size=int(re.search(r'\[sizeof=(\d+),',context_block)[1])
assert table_start==((work+context_size+4095)&~4095)
assert work<table_start<table_end
ranges=[(param('mem_low'),param('mem_high'),0x7fc,0),(table_start,table_end,0x77c,1<<54)]
if sealed:
ranges=[(lo,hi,attrs,0) for lo,hi,attrs in (
(param('mem_low'),const_begin,0x7fc),(const_begin,const_end,0x77c),
(const_end,param('mem_high'),0x7fc)) if lo<hi]+[(table_start,table_end,0x77c,1<<54)]
# Independently verify the explicit virtual TXM capability. The
# negative endpoint7 reply must not conceal any published backing.
txm_ro=read(param('txm_ro_pa'),0x4000)
channel_descriptor=struct.unpack_from('<3Q',txm_ro,0x38)
assert txm_ro[0xa]==0 and channel_descriptor==(0,0,0)
assert tree.get('/device-tree/arm-io/dart-sep','txm-secure-channel-base') is None
txm_sep_channel=dict(platform_class=txm_ro[0xa],available=False,
descriptor=list(channel_descriptor),device_tree_iova=None,
expected_txm_status=0x29)
trust_word=offset('params','trust_cache')//8
trust_cache=words[trust_word:trust_word+3]
trust_audit=runpy.run_path(str(ROOT/'tools/audit-static-trust.py'))
static_trust=trust_audit['audit_static_trust'](read,trust_cache,
read(work+offset('context','trust_state'),616),trust_spec,txm_dynamic_expected)
if trust_cache[0]:
tc_lo=trust_cache[0]
tc_hi=tc_lo+0x4000+((trust_cache[2]+0x3fff)&~0x3fff)
window=param('txm_aspace_pa')+TXM_ARENA_BYTES+3*0x4000<=tc_lo<tc_hi<=param('scratch_pa')-0x4000
payload=(installed is not None and
param('scratch_pa')+6*0x8000<=tc_lo<tc_hi<=param('alloc_low'))
assert window or payload, 'trust cache outside loader reservations'
assert not (tc_lo&0x3fff)
assert param('mem_low')<=tc_lo<tc_hi<=param('mem_high')
if payload:
assert tc_hi<=tree_pa or tree_pa+tree_len<=tc_lo
if linked_auxkc:
assert tc_hi<=aux_pa or aux_pa_end<=tc_lo
sep_lo=param('sep_firmware_pa');sep_hi=sep_lo+param('sep_firmware_size')
assert sep_lo==sep_hi or tc_hi<=sep_lo or sep_hi<=tc_lo
static_trust['placement']='payload' if payload else 'window'
split=[];covered=False
for lo,hi,attrs,upper in ranges:
if lo<=tc_lo<tc_hi<=hi:
assert not covered and attrs==0x7fc and not upper
covered=True
split += [(a,b,attr,upper) for a,b,attr in
((lo,tc_lo,attrs),(tc_lo,tc_hi,0x77c),(tc_hi,hi,attrs)) if a<b]
else:split.append((lo,hi,attrs,upper))
assert covered
ranges=split
panic_lo=param('scratch_pa')-0x4000;panic_hi=param('scratch_pa')
assert param('txm_aspace_pa')+TXM_ARENA_BYTES+3*0x4000<=panic_lo<panic_hi<=param('mem_high')
panic_va=param('ram_aperture_va')+panic_lo-param('mem_low')
panic_audit=runpy.run_path(str(ROOT/'tools/audit-panic-state.py'))
panic_state=panic_audit['audit'](read(panic_lo,0x4000),
read(work+offset('context','panic_state'),88),q(param('watch_pa')+0x198),panic_va)
panic_state['pa']=hex(panic_lo)
def kernel_pa(va):
table=geometry['root']
for shift in (36,25,14):
assert geometry['ram_low']<=table<=geometry['ram_high']-16384 and not table&16383
desc=q(table+8*((va>>shift)&2047))
assert desc&3==3
table=desc&0x0000ffffffffc000
pa=table+(va&16383)
assert geometry['ram_low']<=pa<=geometry['ram_high']-8
return pa
assert kernel_pa(panic_va)==panic_lo
if user_execution['returns']:
# Exact 26A428 initialization copy and the EL0 handler's global.
copied=q(kernel_pa(0xfffffe0007da0278+0x198))
global_pointer=q(kernel_pa(0xfffffe0007da05d0))
assert copied==global_pointer==panic_va
panic_state.update(kernel_copy=hex(copied),kernel_global=hex(global_pointer))
split=[];covered=False
for lo,hi,attrs,upper in ranges:
if lo<=panic_lo<panic_hi<=hi:
assert not covered and attrs==0x7fc and not upper
covered=True
split += [(a,b,attr,xn) for a,b,attr,xn in
((lo,panic_lo,attrs,upper),(panic_lo,panic_hi,0x77c,1<<54),
(panic_hi,hi,attrs,upper)) if a<b]
else:split.append((lo,hi,attrs,upper))
assert covered
ranges=split
const_region=dict(begin=hex(const_begin),end_exclusive=hex(const_end),
sealed_cpu_mask=hex(sealed),lockdowns=lockdowns,
tlb_invalidations=invalidations,register_reads=list(reads))
user_mapping=dict(live_tables=q(work+offset('context','user_live_tables')),
live_pages=q(work+offset('context','user_live_pages')),
missing_tables=q(work+offset('context','user_missing_tables')),
unmap_calls=q(work+offset('context','user_unmap_calls')),
unmap_pages=q(work+offset('context','user_unmap_pages')),
condemn_calls=q(work+offset('context','user_condemn_calls')),
uncondemn_calls=q(work+offset('context','user_uncondemn_calls')),
detach_calls=q(work+offset('context','user_detach_calls')),
comm_attach_calls=q(work+offset('context','user_comm_attach_calls')),
comm_attach_leaves=q(work+offset('context','user_comm_attach_leaves')))
if param('fb_span'):ranges.append((param('fb_pa'),param('fb_pa')+param('fb_span'),0x7fc,1<<54))
# Stage 4: an Adreno profile's device passthrough (vcpu_stage2_prepare, probes/adreno.py).
if re.search(r'^\s*\d+ \| struct adreno_params gpu$',layout,re.M):
import adreno
ranges+=adreno.stage2_audit_ranges(param('gpu'))
# --input: the keyboard's GENI page (vcpu_stage2_prepare, compat/i2c-input.h), from the block a
# -DSNAP_I2C_INPUT=1 monitor reads right after struct params; words that are not a valid block
# (any other monitor) add nothing, as that monitor maps nothing.
import i2cinput
i2c_rows=i2cinput.stage2_audit_ranges(list(struct.unpack(f'<{i2cinput.PARAMS_WORDS}Q',
read(param_pa+xb.PARAMS_WORDS*8,i2cinput.PARAMS_WORDS*8))))
ranges+=i2c_rows
# --qcc2072-wifi: the five QCC2072 windows (vcpu_stage2_prepare, compat/qcc2072-wifi.h), from the
# block a -DSNAP_QCC2072_WIFI=1 monitor reads right after struct params and any input block;
# words that are not a valid block (any other monitor) add nothing, as that monitor maps nothing.
import qcc2072wifi
qcc_at=param_pa+(xb.PARAMS_WORDS+(i2cinput.PARAMS_WORDS if i2c_rows else 0))*8
qcc_rows=qcc2072wifi.stage2_audit_ranges(list(struct.unpack(f'<{qcc2072wifi.PARAMS_WORDS}Q',
read(qcc_at,qcc2072wifi.PARAMS_WORDS*8))))
ranges+=qcc_rows
# --present-beam: the DPU INTF_5 page, read-only (vcpu_stage2_prepare, compat/display-timing.h), from the
# block a -DSNAP_DISPLAY_TIMING=1 monitor reads after struct params and any QCC2072 block; words that are not
# a valid block (any other monitor) add nothing, as that monitor maps nothing.
import displaytiming
dt_at=qcc_at+(qcc2072wifi.PARAMS_WORDS*8 if qcc_rows else 0)
ranges+=displaytiming.stage2_audit_ranges(list(struct.unpack(f'<{displaytiming.PARAMS_WORDS}Q',
read(dt_at,displaytiming.PARAMS_WORDS*8))))
stage2=audit_private_stage2(read,banks,ranges,table_start,table_end)
stage2['private_context_unmapped']=True
stage2['private_context_range']=[hex(work),hex(table_start)]
assert not runtime_pages&{int(x,16) for x in stage2['table_addresses']}
base=q(work+offset('context','secondary_base'));assert work<=base<table_end-6*4096
physical=[];launched=0
for slot in range(1,6):
record=struct.unpack('<37Q',read(base+slot*4096,37*8))
row=dict(zip(secondary['FIELDS'],record));row['slot']=slot
if cpu_rows[slot]['prepared']:
assert row['phase']==4 and row['cleanup']==1 and row['affinity_result']==1
assert row['on_result']==row['prepare_result']==row['finish_result']==row['error']==0
assert row['entered']==1 and row['active_vmpidr']==0x80000000|(slot if slot<4 else slot-4+0x100)
assert row['active_vncr']==int(cpu_rows[slot]['vncr'],16)
assert row['active_vttbr']==banks[slot]['vttbr']
assert tuple(record[:4])==tuple(record[26:30])
launched|=1<<slot
else:assert row['phase']==row['entered']==0
physical.append(row)
armed,issued=struct.unpack('<2Q',read(work+offset('context','cpu_release'),16))
assert issued==launched and armed&launched==launched
assert launched or args.allow_unlaunched
cpu_release=dict(armed=armed,issued=issued,launched=launched,
secondary_execution_verified=bool(launched))
assert not proxy.exceptions
nonce_base=work+offset('context','txm_nonces')
nonce_entries=[]
for index in range(12):
# Header only: never export the two sixteen-byte material fields.
domain,flags,initialized=struct.unpack('<QII',read(nonce_base+48*index,16))
assert initialized in (0,1) and not flags&~0x7f
if initialized:nonce_entries.append(dict(index=index,domain_id=hex(domain),flags=hex(flags)))
nonce_sets,nonce_rejections=struct.unpack('<2Q',read(nonce_base+12*48,16))
assert nonce_sets==len(nonce_entries)
digest_base=work+offset('context','txm_nonce_digests')
digest_entries=[]
for index in range(17):
# Read only the length: digest material remains inside the monitor.
length=q(digest_base+index*56+48)
assert length<=48
if length:digest_entries.append(dict(domain_index=index,length=length))
digest_sets=q(digest_base+952)
assert digest_sets==len(digest_entries)
# Reconstruct identities independently from the typed frame map and
# guest-visible TXM objects. Never infer conversion success from its
# counter alone: object handle, packed class/id and mapping pointer
# must agree, with unique roots and class-specific identifiers.
registry_base=work+offset('context','txm_registry')
registry_count=struct.unpack('<I',read(registry_base+offset('txm_registry','count'),4))[0]
L=TXM_ASPACE_LIMIT
assert offset('txm_registry','asid')==8*L and offset('txm_registry','shared')-offset('txm_registry','count')>=4
assert registry_count<=L
registry_roots=struct.unpack(f'<{L}Q',read(registry_base,8*L))
registry_ids=struct.unpack(f'<{L}H',read(registry_base+8*L,2*L))
registry_shared=read(registry_base+offset('txm_registry','shared'),L)
registry_starts=struct.unpack(f'<{L}Q',read(registry_base+offset('txm_registry','region_start'),8*L))
registry_ends=struct.unpack(f'<{L}Q',read(registry_base+offset('txm_registry','region_end'),8*L))
region_sets=q(registry_base+offset('txm_registry','region_sets'))
registry_bound=struct.unpack(f'<{L}Q',read(registry_base+offset('txm_registry','bound_shared'),8*L))
binding_sets=q(registry_base+offset('txm_registry','bindings'))
binding_releases=q(registry_base+offset('txm_registry','unbindings'))
arena_pa=words[offset('params','txm_aspace_pa')//8]
arena_va=geometry['aperture_va']+arena_pa-geometry['ram_low']
txm_objects=[];identities=set();object_roots=set()
for i in range(registry_count):
pa=registry_roots[i];identity=registry_ids[i];kind=registry_shared[i]
region_start,region_end=registry_starts[i],registry_ends[i]
assert kind in (0,1)
if not pa:
assert identity==kind==0
assert region_start==region_end==registry_bound[i]==0
continue
if region_start or region_end:
assert kind==1 and 0<region_start<region_end<=0xffffffffffffffff
assert not (region_start|region_end)&0x3fff
assert pa not in object_roots and (kind,identity) not in identities
object_roots.add(pa);identities.add((kind,identity))
assert pa in (shared if kind else users)
f=fm[((pa-geometry['frame_low'])>>14)*16:((pa-geometry['frame_low'])>>14)*16+16]
assert identity==(f[8] if kind else int.from_bytes(f[4:6],'little'))
handle=arena_va+i*256
obj=read(arena_pa+i*256,256)
expected=bytearray(256);expected[0x11]=expected[0x91]=1
struct.pack_into('<HH',expected,0x22,identity,kind)
struct.pack_into('<Q',expected,0x38,handle+0x80)
struct.pack_into('<2Q',expected,0x58,region_start,region_end)
references=sum(bool(registry_roots[j]) and registry_bound[j]==pa for j in range(registry_count))
assert not references or kind==1
struct.pack_into('<I',expected,0x68,references)
if kind:
assert not registry_bound[i]
else:
assert (f[8]==255)==(registry_bound[i]==0)
if registry_bound[i]:
parent=registry_roots.index(registry_bound[i])
assert parent<registry_count and registry_shared[parent]==1
assert registry_ids[parent]==f[8]
pf=fm[((registry_bound[i]-geometry['frame_low'])>>14)*16:((registry_bound[i]-geometry['frame_low'])>>14)*16+16]
assert pf[12]==f[12] and int.from_bytes(pf[4:6],'little')>0
assert registry_starts[parent]<registry_ends[parent] and registry_starts[parent]>0
struct.pack_into('<Q',expected,0x28,handle+0x80)
struct.pack_into('<Q',expected,0x38,0)
struct.pack_into('<Q',expected,0x48,handle+0x80)
expected[0x92]=1
struct.pack_into('<Q',expected,0x98,arena_va+parent*256)
struct.pack_into('<2Q',expected,0xa8,registry_starts[parent],registry_ends[parent])
assert obj==expected, 'TXM object disagrees with its registered identity'
txm_objects.append(dict(registry_slot=i,root=hex(pa),identity=identity,
shared=bool(kind),handle=hex(handle),metadata=f.hex(),
region_start=hex(region_start),region_end=hex(region_end),
bound_shared=hex(registry_bound[i]),binding_references=references))
assert region_sets>=sum(bool(registry_ends[i]) for i in range(registry_count))
assert binding_sets-binding_releases==sum(bool(registry_bound[i]) for i in range(registry_count))
shared_root_state=dict(converts=q(work+offset('context','shared_root_converts')),
releases=q(work+offset('context','shared_root_releases')),region_sets=region_sets,
bindings=binding_sets,unbindings=binding_releases,objects=txm_objects)
root_config_audit=runpy.run_path(str(ROOT/'tools/audit-root-configuration.py'))
root_configuration=root_config_audit['audit_root_configuration'](
read(work+offset('context','root_configuration'),64),
{pa:fm[((pa-geometry['frame_low'])>>14)*16:((pa-geometry['frame_low'])>>14)*16+16] for pa in users})
shared_base=work+offset('context','shared_regions')
shared_metadata={pa:fm[((pa-geometry['frame_low'])>>14)*16:
((pa-geometry['frame_low'])>>14)*16+16] for pa in shared}
shared_audit=runpy.run_path(str(ROOT/'tools/audit-shared-regions.py'))
sptm_shared_regions=shared_audit['audit_shared_regions'](
read(shared_base,255*32),
q(shared_base+offset('sptm_shared_registry','configurations')),shared_metadata)
shared_pages=shared_page_auditor['audit'](read,state=shared_page_state,cache=cached,
regions=sptm_shared_regions['entries'],ram_low=geometry['ram_low'],ram_high=geometry['ram_high'],
work_low=work,work_high=q(work+offset('context','next_table')))
shared_tables=[]
for pa in managed.frames():
i=((pa-geometry['frame_low'])>>14)*16;f=fm[i:i+16]
if f[2]==21:
shared_tables.append(dict(pa=hex(pa),level=f[4],
parent_links=int.from_bytes(f[6:8],'little'),
child_references=int.from_bytes(f[8:10],'little')))
shared_table_state=dict(tables=len(shared_tables),
levels={str(level):sum(t['level']==level for t in shared_tables) for level in range(4)},
entries=shared_tables)
release_auditor=runpy.run_path(str(ROOT/'tools/audit-txm-release-type.py'))
txm_release_type=release_auditor['audit'](
read(work+offset('context','txm_release_type'),184),
stacks_pa=param('txm_stacks_pa'),stack_count=param('txm_stack_count'))
key_auditor=runpy.run_path(str(ROOT/'tools/audit-txm-local-key.py'))
txm_local_key=key_auditor['audit'](
read(work+offset('context','txm_local_key'),128),
stacks_pa=param('txm_stacks_pa'),stack_count=param('txm_stack_count'))
result=dict(guest_profile=xb.PROFILE_NAME,
native_calls=native_report,
native_writers=writer_report,
runtime_stop_qualified=not args.diagnose_primary_timeout,
user_mapping_qualified=user_mapping_qualified,
service_budget_stops=service_budget_stops,
early_stop=early_stop,
monitor_sha256=hashlib.sha256(blob).hexdigest(),geometry=geometry,
txm_image4_uuid_rejections=struct.unpack('<Q',read(work+offset('context','txm_image4_uuid_rejections'),8))[0],
txm_release_type=txm_release_type,
txm_local_key=txm_local_key,
txm_native=txm_native_report,
txm_nonces=dict(sets=nonce_sets,rejections=nonce_rejections,entries=nonce_entries),
txm_nonce_digests=dict(sets=digest_sets,entries=digest_entries),
shared_root_state=shared_root_state,
root_configuration=root_configuration,
sptm_shared_regions=sptm_shared_regions,
shared_table_state=shared_table_state,
nesting=nesting,
unnesting=unnesting,shared_detaches=shared_detaches,user_regions=user_regions,
shared_regions_updates=shared_regions_updates,
user_disjoint_unmaps=user_disjoint_unmaps,
kernel_disjoint_unmaps=kernel_disjoint_unmaps,
user_map_replacements=user_map_replacements,
static_trust=static_trust,
txm_sep_channel=txm_sep_channel,
root_switch=root_switch,el0_crashes=el0_crashes,pv_audit=pv_audit,
linked_auxkc=linked_auxkc,
const_region=const_region,user_mapping=user_mapping,
kernel_permissions=kernel_permissions,user_execution=user_execution,panic_state=panic_state,
pan_entry=pan_entry,
user_clock=user_clock,
user_map_conflict=user_map_conflict,
user_sprr=user_sprr,
uperm_write=uperm_write,
pperm_copy=pperm_copy,
user_permissions=dict(roots=user_permissions,cpu_state=view_states,
protects=tpro_protects,releases=tpro_releases,live_frames=tpro_live),
native_tables=dict(enabled=native_tables,stats=native_stats,cpus=native_cpus,
source_pages=sum(bin(b).count('1') for b in native_sources_raw)),
native_shared=native_shared,
user_updates=user_updates,
kernel_updates=kernel_updates,
shared_pages=shared_pages,
ownership=ownership,dart=dart,service=list(service),cpu_rows=cpu_rows,
stage2=stage2,physical_secondaries=physical,cpu_release=cpu_release,
qspi=qspi,dwi=dwi,i2c=i2c,pmp=pmp,spmi=spmi,watchdog_smc=watchdog_smc,aes_idle=aes_idle,aes_software=aes_software,aes_key_disable=aes_key_disable,
sep_aon=sep_aon,sep_asc=sep_asc,dart_runtime=dart_runtime,acc_sampler=acc_sampler,ans=ans,
cpu_perf_reads=cpu_perf_reads,firmware=firmware)
finally:proxy.link.close()
output=ROOT/'build'/f'{args.prefix}-audit.json'
output.write_text(json.dumps(result,indent=2)+'\n')
summary={k:v for k,v in ownership.items() if k!='mismatches'}
summary['mismatch_count']=len(ownership['mismatches'])
summary['first_mismatches']=ownership['mismatches'][:8]
print(json.dumps(dict(ownership=summary,dma_mismatches=dart['mismatches'][:8],
user_mapping_qualified=user_mapping_qualified,
private_table_pages=stage2['disjoint_table_pages'],pv_audit=pv_audit,
linked_auxkc=linked_auxkc,
active_cpu_slots=[r['slot'] for r in cpu_rows if r['prepared']],qspi=qspi,dwi=dwi,i2c=i2c,pmp=pmp,output=str(output))))
return bool(ownership['mismatches'] or dart['mismatches'] or not user_mapping_qualified)
if __name__=='__main__':raise SystemExit(main())