Files
UniXNU/scripts/rust-pac-rewrite.py
rpone ab11a8cb68 feat(scripts): iPadOS 27 kernel, driver, GPU addon and seed build steps
Source-driver and source-storage builds, the arm64e PAC rewrite for the Rust bridge, the IOAccelerator base ABI of 24A446, the bridge with mimalloc and link-time optimisation, the plugin with the prime list, addon options for a prime list, a pipeline pack and cache export, wallpaper overlay and seed changes, and payload, kernel and userdata steps for the new profile.
2026-10-28 09:20:55 +08:00

77 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Route the indirect branches of the linked Rust code of an arm64e image through the stripping trampolines.
rust-pac-rewrite.py IMAGE MAP ARCHIVE_NAME
rustc's arm64e code calls function pointers with a plain blr/br, which faults on the signed pointers the rest of an arm64e process hands
out (drivers/AdrenoMetal/bridge/RustPacBoundary.c). Every blr Xn / br Xn inside a function the link map attributes to an object of
ARCHIVE_NAME becomes bl / b to nmb_pac_call_xN (xpaci; br), in place, before the image is signed. blr/br x30 is left alone and counted."""
import re
import struct
import sys
from pathlib import Path
if len(sys.argv) != 4:
sys.exit(__doc__)
image, link_map, archive = Path(sys.argv[1]), Path(sys.argv[2]).read_text(errors='replace'), sys.argv[3]
data = bytearray(image.read_bytes())
files, text, symbols, part = set(), None, [], None
for line in link_map.splitlines():
if line.startswith('# Object files:'): part = 'files'; continue
if line.startswith('# Sections:'): part = 'sections'; continue
if line.startswith('# Symbols:'): part = 'symbols'; continue
if line.startswith('# Dead Stripped'): part = None; continue
if line.startswith('#') or not line.strip(): continue
if part == 'files':
m = re.match(r'\[\s*(\d+)\]\s+(.*)$', line)
if m and archive in m.group(2): files.add(int(m.group(1)))
elif part == 'sections':
f = line.split()
if len(f) >= 4 and f[2] == '__TEXT' and f[3] == '__text': text = (int(f[0], 16), int(f[1], 16))
elif part == 'symbols':
m = re.match(r'(0x[0-9A-Fa-f]+)\s+(0x[0-9A-Fa-f]+)\s+\[\s*(\d+)\]\s+(.*)$', line)
if m: symbols.append((int(m.group(1), 16), int(m.group(2), 16), int(m.group(3)), m.group(4)))
if not files or not text:
sys.exit('the map names no object of %s or no __TEXT,__text' % archive)
tramp = {}
for address, size, index, name in symbols:
m = re.fullmatch(r'_nmb_pac_call_x(\d+)', name)
if m: tramp[int(m.group(1))] = address
if sorted(tramp) != list(range(30)):
sys.exit('trampolines missing: %s' % sorted(set(range(30)) - set(tramp)))
# __TEXT segment: virtual address to file offset.
ncmds = struct.unpack_from('<I', data, 16)[0]
at, segment = 32, None
for _ in range(ncmds):
cmd, size = struct.unpack_from('<II', data, at)
if cmd == 0x19 and data[at + 8:at + 24].rstrip(b'\0') == b'__TEXT':
vmaddr, vmsize, fileoff = struct.unpack_from('<QQQ', data, at + 24)
segment = (vmaddr, vmsize, fileoff)
at += size
vmaddr, vmsize, fileoff = segment
lo, hi = text[0], text[0] + text[1]
counts = {'blr': 0, 'br': 0, 'x30': 0}
for address, size, index, name in symbols:
if index not in files or not lo <= address < hi:
continue
for pc in range(address, min(address + size, hi), 4):
off = pc - vmaddr + fileoff
word = struct.unpack_from('<I', data, off)[0]
kind = {0xD63F0000: 'blr', 0xD61F0000: 'br'}.get(word & 0xFFFFFC1F)
if not kind:
continue
reg = (word >> 5) & 31
if reg == 30:
counts['x30'] += 1
continue
delta = (tramp[reg] - pc) >> 2
if not -(1 << 25) <= delta < (1 << 25):
sys.exit('trampoline out of branch range at %#x' % pc)
struct.pack_into('<I', data, off, (0x94000000 if kind == 'blr' else 0x14000000) | (delta & 0x3FFFFFF))
counts[kind] += 1
image.write_bytes(data)
print('PASS: Rust indirect branches routed through the PAC trampolines: %d blr, %d br, %d x30 left (%d objects)'
% (counts['blr'], counts['br'], counts['x30'], len(files)))