Files
UniXNU/scripts/prepare-original-cpu-render-observer.py
2026-10-19 18:49:31 +08:00

157 lines
11 KiB
Python

#!/usr/bin/env python3
"""Bind read-only original CPU-render observations to a qualified kernel/cache.
The layout plan must have been qualified for this exact native kernel, and its
four compiler objects must still match. This accepts retained prelink candidates
without silently borrowing the offsets/entrypoints of a subsequently rebuilt one.
"""
import argparse
import hashlib
import json
import mmap
from pathlib import Path
import re
import struct
import subprocess
ROOT = Path(__file__).resolve().parents[1]
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--kernel', type=Path, required=True)
p.add_argument('--prelink-report', type=Path, required=True)
p.add_argument('--layout-plan', type=Path, required=True)
p.add_argument('--output', type=Path, required=True)
mode = p.add_mutually_exclusive_group()
mode.add_argument('--trace', action='store_true')
mode.add_argument('--frame-path', action='store_true', help='Observe real renderer construction, CPU drawing and framebuffer submission')
mode.add_argument('--gates', action='store_true', help='Observe actual display enabled/update/ready decisions')
mode.add_argument('--composition', action='store_true', help='Observe original update filtering and damage decisions')
mode.add_argument('--mode-state', action='store_true', help='Observe original framebuffer mode inputs and update-filter state')
a = p.parse_args()
plan = json.loads(a.layout_plan.read_text())
report = json.loads(a.prelink_report.read_text())
def require(condition, message):
if not condition:
raise SystemExit(message)
require(plan['native_sha256'] == report['native_sha256'], 'Layout plan belongs to a different native kernel')
require(hashlib.sha256(a.kernel.read_bytes()).hexdigest() == report['candidate_sha256'], 'Candidate differs from prelink report')
require(plan['original_files_modified'] is False, 'Original images must remain unchanged')
objects = ROOT / 'build/xnu/ios-obj/DEVELOPMENT_ARM64_BCM2837'
for key, relative in [('thread', 'osfmk/DEVELOPMENT/thread.o'), ('proc', 'bsd/DEVELOPMENT/kern_proc.o'),
('task', 'osfmk/DEVELOPMENT/task.o'), ('region', 'osfmk/DEVELOPMENT/vm_shared_region.o')]:
require(hashlib.sha256((objects / relative).read_bytes()).hexdigest() == plan['process_layout'][key + '_object_sha256'],
'Compiler layout object changed: ' + key)
launchd = ROOT / '.cache/firmware/target-volumes/os-mount/sbin/launchd'
require(hashlib.sha256(launchd.read_bytes()).hexdigest() == plan['launchd_sha256'] ==
'8cec9a3c44a443b08e977ccc5d577850316ad428c575dc01f09b018dbd5b6a25', 'Original launchd differs')
cache = ROOT / '.cache/firmware/target-volumes/systemos-mount/System/Library/Caches/com.apple.dyld'
with (cache / 'dyld_shared_cache_arm64').open('rb') as f:
header = f.read(416)
require(header[88:104].hex() == plan['cache_uuid'] == '590d839a82d230f1a17d0cc053420f73', 'Original cache UUID differs')
require(header[400:416].hex() == plan['local_symbols_uuid'] == 'a1a859a2479c31998a34f4afbd808cf5', 'Original symbol UUID differs')
specs = [
('display_timer', 0x1894f5a0c, 0x3d8, '9d2dd9e0c74e07f519709ce39629fbfe4399fb4774c18ea4273a76a3ce8b3b20'),
('render_for_time', 0x18951dde0, 0x3f1c, '94cae733f1ee23252d773b75d701077f85c07417b7647da214f208e7d09cec4a'),
('render_update', 0x1894c5eb0, 0xac, 'b63dc0ec6492a9e1a7343f91bc7bae0f3a710ce2c2b13081931a7c4a646e745f'),
('software_renderer', 0x1896a880c, 0x1d8, '8b6a57aa9f177cc237b23123fba8033254b8ad5d03f37de221febdb4bdf0a821'),
('can_update', 0x189509d88, 0xb8, '083e024d55d67a0950c0a0f3665f3a553fbed0115c413a6391ce2da10ff41bd7'),
('is_ready', 0x189510848, 0x88, '6b946552ee020aca1c74dd310acb89403f62a6a6c85e9836f5c941a807ebb518'),
('ignore_update', 0x18952288c, 0x5c, '5df7f40779b4a601c4f8f55319ec00bde52b8b140a74b33fee89d4512df695f1'),
('update_framebuffer', 0x189596aec, 0x20fc, 'eeae0b7e58ac0d9225ad0e5658ace97f930b3d29632639637634dd80f5db1067'),
('render_display', 0x1894c60e0, 0x340, '33dcc0cbda369b002c2c669a2c55518761bbf3623901064ac182b529fb5f74ba'),
('software_draw_elements', 0x189616220, 0x17ac, 'cfabb66aafc1f4744400d4ee3917b1c517a1a98f15859785cccf62b6525034ab'),
('framebuffer_swap_end', 0x1b63cfc94, 0x1c, 'e34e888204aa63584884512f17a59a579456219676dd23b120424b425fd88815'),
]
found = {}
gate_prefixes = {}
for file in sorted(cache.glob('dyld_shared_cache_arm64*')):
if file.suffix == '.symbols':
continue
with file.open('rb') as f, mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as data:
require(data[:16].rstrip(b'\0') == b'dyld_v1 arm64', 'Require ordinary ARM64 cache')
at, count = struct.unpack_from('<2I', data, 16)
for index in range(count):
va, size, offset, _, protection = struct.unpack_from('<3Q2I', data, at + index * 32)
for name, address, length, digest in specs:
if va <= address and address + length <= va + size:
body = data[offset + address - va:offset + address - va + length]
require(protection & 4 and hashlib.sha256(body).hexdigest() == digest, 'Original function differs: ' + name)
require(name not in found, 'Ambiguous executable cache mapping')
found[name] = dict(name=name, address=address, length=length, sha256=digest, prefix=body[:16].hex())
if name == 'software_renderer':
require(body[0x1d0:0x1d4] == bytes.fromhex('c0035fd6'), 'Original software renderer RET differs')
renderer_return_prefix = data[offset + address - va + 0x1d0:offset + address - va + 0x1e0].hex()
for gate, parent, delta in [('can_update_enabled_result', 'can_update', 0x30),
('can_update_return', 'can_update', 0x90),
('is_ready_return', 'is_ready', 0x7c),
('ignore_update_result', 'render_for_time', 0x1194),
('damage_comparison', 'render_for_time', 0x1250)]:
if name == parent:
# Prefix may span the function's final RET. Read it
# from the same real executable cache mapping.
start = offset + address - va + delta
gate_prefixes[gate] = data[start:start + 16].hex()
require(len(found) == len(specs), 'Original render functions are missing')
symbols = subprocess.check_output(['xcrun', 'nm', '-U', a.kernel], text=True, timeout=30)
plan['process_activation'] = []
for name in ('_shared_region_check_np', '_shared_region_map_and_slide_2_np'):
match = re.search(r'^([0-9a-f]+)\s+\S\s+' + name + '$', symbols, re.M)
require(match is not None, 'Candidate shared-region entrypoint is missing')
plan['process_activation'].append(int(match[1], 16))
for key in ('port_checkpoint', 'reply_checkpoint', 'main_display', 'displays', 'cas_request_id', 'original_mode_initializer_sha256', 'trace_sites'):
plan.pop(key, None)
site = found['software_renderer']
plan.update(mode='user-trace' if a.trace or a.gates or a.composition or a.mode_state or a.frame_path else 'user-return', expected_process='backboardd', query=site['address'],
query_prefix=site['prefix'], return_checkpoint=0x1d0, object_bytes=104, original_function_sha256=site['sha256'],
candidate_sha256=report['candidate_sha256'], purpose='Unchanged original backboardd CPU-render path; read-only observation')
plan['max_activation_events'] = 2048
if a.trace:
plan['trace_sites'] = [found[name] for name, *_ in specs[:4]]
if a.frame_path:
plan['trace_sites'] = [dict(name='software_renderer_return',
address=found['software_renderer']['address'] + 0x1d0,
prefix=renderer_return_prefix, parent_function=found['software_renderer'],
memory_reads=[dict(register=0, offset=0, size=104, indirect=False)]),
*[found[name] for name in ('render_display', 'software_draw_elements', 'framebuffer_swap_end')]]
if a.gates:
plan['trace_sites'] = []
for name, parent, delta, register in [('can_update_enabled_result', 'can_update', 0x30, 20),
('can_update_return', 'can_update', 0x90, 20),
('is_ready_return', 'is_ready', 0x7c, 19)]:
# Addresses/fields follow the above hash-qualified original bodies.
# These are observations, never replacements for the original checks.
parent_site = found[parent]
plan['trace_sites'].append(dict(name=name, address=parent_site['address'] + delta,
prefix=gate_prefixes[name], max_hits=8, parent_function=parent_site,
memory_reads=[dict(register=register, offset=offset, size=size, indirect=indirect)
for offset, size, indirect in [(0,64,False), (0x248,8,False), (0x290,16,True), (0x6bcd,32,False)]]))
plan['trace_sites'].append(found['render_update'])
if a.composition:
plan['trace_sites'] = []
for name, delta in [('ignore_update_result', 0x1194), ('damage_comparison', 0x1250)]:
parent_site = found['render_for_time']
plan['trace_sites'].append(dict(name=name, address=parent_site['address'] + delta,
prefix=gate_prefixes[name], max_hits=8, parent_function=parent_site,
memory_reads=[dict(register=19, offset=offset, size=size, indirect=indirect)
for offset, size, indirect in [(0x3d0,256,False), (0x4d4,4,False), (0x1f0,256,True),
(0x4b0,8,False), (0xc8,8,False)]]))
plan['trace_sites'] += [found['render_update'], found['software_renderer']]
if a.mode_state:
plan['trace_sites'] = []
for name in ('update_framebuffer', 'ignore_update'):
# Both exact original function entries take the display in x0. Capture
# mode source/current/selected state and the shared readiness flags.
# A sentinel value alone is not proof of an invalid hardware mode.
site = dict(found[name], max_hits=8,
memory_reads=[dict(register=0, offset=offset, size=size, indirect=indirect)
for offset, size, indirect in [(0x240,48,False), (0x68e0,64,False),
(0x6bc0,64,False), (0x290,16,True)]])
if name == 'ignore_update':
site['memory_reads'].append(dict(register=1, offset=0x100, size=16, indirect=False))
plan['trace_sites'].append(site)
plan['trace_sites'] += [found['render_update'], found['software_renderer']]
a.output.parent.mkdir(parents=True, exist_ok=True)
a.output.write_text(json.dumps(plan, indent=2) + '\n')
print('PASS: original render bytes, cache identity, compiled process layout and exact candidate entrypoints')