Files
UniXNU/scripts/audit-target-air-inputs.py
2026-10-19 18:49:31 +08:00

206 lines
11 KiB
Python

#!/usr/bin/env python3
"""Audit exact original UI AIR inputs; decode only, never compile/execute a GPU shader."""
import concurrent.futures
from collections import Counter
import hashlib
import json
from pathlib import Path
import re
import struct
import subprocess
import time
ROOT = Path(__file__).resolve().parents[1]
BASE = ROOT / '.cache/firmware/target-volumes/os-mount/System/Library'
OUT = ROOT / 'build/gpu-analysis/air'
LLVM_ROOT = Path('/opt/homebrew/opt/llvm')
LLVM_DIS = LLVM_ROOT / 'bin/llvm-dis'
PINS = {
'Frameworks/QuartzCore.framework/default.metallib':
'606f913adf520420f9b4bb666efb782a68c830f082f1e559e17b20d0d13adbc9',
'PrivateFrameworks/RenderBox.framework/default.metallib':
'd76883f626b1527c36fb0940cf27dcade503a55cf2fa05c8f7a247a40ccebec4',
}
def require(value, message):
if not value:
raise ValueError(message)
def extent(data, start, length):
require(0 <= start <= len(data) and 0 <= length <= len(data) - start, 'Original input extent outside file')
return data[start:start + length]
def slices(data):
if data[:4] != b'\xca\xfe\xba\xbe':
return [(dict(offset=0, bytes=len(data)), data)]
n, = struct.unpack_from('>I', data, 4)
require(n == 2, 'Exact target QuartzCore has two bounded universal slices')
records = []
for i in range(n):
cpu, subtype, offset, size, alignment = struct.unpack('>5I', extent(data, 8 + 20 * i, 20))
require(offset >= 8 + 20 * n and alignment <= 16 and not offset % (1 << alignment), 'Bounded original universal slice alignment')
records.append((dict(cpu=hex(cpu), subtype=hex(subtype), offset=offset, bytes=size), extent(data, offset, size)))
require(records[0][0]['offset'] + records[0][0]['bytes'] <= records[1][0]['offset'], 'Disjoint original universal slices')
return records
def entries(data):
require(data[:16] == bytes.fromhex('4d544c42010002000700008211000700') and
struct.unpack_from('<Q', data, 16)[0] == len(data), 'Exact target MTLB header profile')
sections = [tuple(pair) for pair in struct.iter_unpack('<2Q', extent(data, 24, 64))]
for at, size in sections:
extent(data, at, size)
for (at, size), (next_at, _) in zip(sections, sections[1:]):
require(88 <= at <= at + size <= next_at, 'Disjoint ordered original MTLB sections')
at, length = sections[0]
count, = struct.unpack('<I', extent(data, at, 4))
require(0 < count <= 4096, 'Bounded nonempty original UI function inventory')
cursor, records = at + 4, []
for index in range(count):
record_size, = struct.unpack('<I', extent(data, cursor, 4))
require(8 <= record_size <= at + 4 + length - cursor, 'Bounded original function record')
end, tag_at = cursor + record_size, cursor + 4
tags = {}
while tag_at < end:
tag = extent(data, tag_at, 4)
tag_at += 4
if tag == b'ENDT':
require(tag_at == end, 'Exact original record terminator')
break
n, = struct.unpack('<H', extent(data, tag_at, 2))
tag_at += 2
require(tag not in tags and tag_at + n <= end, 'Bounded unique original record tag')
tags[tag] = extent(data, tag_at, n)
tag_at += n
require(tag == b'ENDT' and all(t in tags for t in (b'NAME', b'TYPE', b'HASH', b'OFFT', b'MDSZ')), 'Original AIR record fields')
name = tags[b'NAME']
require(name.endswith(b'\0') and b'\0' not in name[:-1] and len(tags[b'TYPE']) == 1 and len(tags[b'HASH']) == 32,
'Exact original function name/type/hash field sizes')
public, private, bitcode = struct.unpack('<3Q', tags[b'OFFT'])
size, = struct.unpack('<Q', tags[b'MDSZ'])
require(public < sections[1][1] and private < sections[2][1] and
20 <= size <= sections[3][1] - bitcode and 0 <= bitcode < sections[3][1], 'Bounded original metadata/module offsets')
wrapper = extent(data, sections[3][0] + bitcode, size)
require(hashlib.sha256(wrapper).digest() == tags[b'HASH'], 'Original complete module HASH tag must match its bytes')
magic, version, offset, bc_size, cpu = struct.unpack_from('<5I', wrapper)
require(magic == 0x0b17c0de and version == 0 and offset >= 20 and bc_size > 4 and cpu == 0xffffffff,
'Supported original LLVM bitcode wrapper')
bc = extent(wrapper, offset, bc_size)
require(bc[:4] == b'BC\xc0\xde', 'Actual LLVM bitcode signature, not a coincidental text string')
records.append(dict(index=index, name=name[:-1].decode('ascii'), type=tags[b'TYPE'][0],
module_offset=bitcode, module_bytes=size, wrapper_sha256=tags[b'HASH'].hex(),
bitcode_sha256=hashlib.sha256(bc).hexdigest(), bitcode=bc))
cursor = end
require(cursor == at + 4 + length and cursor + 48 == sections[1][0],
'Exact original function bytes: declared size excludes the four-byte record count')
require(data[cursor:cursor + 6] == b'RLST\x10\x00' and data[cursor + 22:cursor + 28] == b'UUID\x10\x00' and
data[cursor + 44:cursor + 48] == b'ENDT', 'Exact original reflection/UUID trailer profile')
reflection, reflection_size = struct.unpack_from('<2Q', data, cursor + 6)
require(reflection == sections[3][0] + sections[3][1] and reflection + reflection_size == len(data),
'Original trailing reflection extent, not an unbounded bitcode scan')
intervals = sorted(set((r['module_offset'], r['module_offset'] + r['module_bytes']) for r in records))
require(all(a[1] <= b[0] for a, b in zip(intervals, intervals[1:])), 'Original module extents do not partially overlap')
return records
OUT.mkdir(parents=True, exist_ok=True)
version = subprocess.check_output([LLVM_DIS, '--version'], text=True).strip()
require('LLVM version 23.1.3' in version, 'Qualify this audit with the actual inspected host LLVM version')
inputs, jobs = [], []
for file, expected in PINS.items():
data = (BASE / file).read_bytes()
require(hashlib.sha256(data).hexdigest() == expected, 'Exact unchanged target library: ' + file)
library = Path(file).parent.stem
views = slices(data)
# The observed QuartzCore AIR-bearing slice is the first. Its classification
# comes from real decoded triples, not assumptions about numeric CPU types.
record, selected = views[0]
modules = entries(selected)
inputs.append(dict(file=file, sha256=expected, bytes=len(data), slices=[r for r, _ in views],
selected_slice=record, functions=len(modules)))
for module in modules:
module['library'] = library
jobs.append(module)
# Verify the original triple in-process through LLVM's module API, without
# creating a target machine or overriding AIR with another architecture.
verifier_source = OUT / 'verify-air.c'
verifier_source.write_text(r'''#include <llvm-c/Analysis.h>
#include <llvm-c/BitReader.h>
#include <llvm-c/Core.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc != 2) return 2;
LLVMMemoryBufferRef buffer = NULL;
LLVMModuleRef module = NULL;
char *message = NULL;
LLVMContextRef context = LLVMContextCreate();
int status = 1;
if (LLVMCreateMemoryBufferWithContentsOfFile(argv[1], &buffer, &message)) goto done;
if (LLVMParseBitcodeInContext2(context, buffer, &module)) goto done;
if (strcmp(LLVMGetTarget(module), "air64-apple-ios17.7.0")) goto done;
if (LLVMVerifyModule(module, LLVMReturnStatusAction, &message)) goto done;
if (strcmp(LLVMGetTarget(module), "air64-apple-ios17.7.0")) goto done;
status = 0;
done:
if (message) { fputs(message, stderr); LLVMDisposeMessage(message); }
if (module) LLVMDisposeModule(module);
if (buffer) LLVMDisposeMemoryBuffer(buffer);
LLVMContextDispose(context);
return status;
}
''')
verifier = OUT / 'verify-air'
subprocess.run(['xcrun', 'clang', '-std=c11', '-O2', '-Wall', '-Wextra', '-Werror',
'-I', LLVM_ROOT / 'include', verifier_source, '-L', LLVM_ROOT / 'lib',
'-lLLVM', '-Wl,-rpath,' + str(LLVM_ROOT / 'lib'), '-o', verifier], check=True, timeout=30)
deadline = time.monotonic() + 60
def decode(module):
require(time.monotonic() < deadline, 'Bounded whole-library decoding deadline')
bc = module.pop('bitcode')
prefix = OUT / module['library'] / ('module-' + str(module['index']))
prefix.parent.mkdir(parents=True, exist_ok=True)
prefix.with_suffix('.bc').write_bytes(bc)
result = subprocess.run([LLVM_DIS, prefix.with_suffix('.bc'), '-o', prefix.with_suffix('.ll')],
text=True, capture_output=True, timeout=8)
require(result.returncode == 0, 'Actual LLVM bitcode decoding failed: ' + module['library'] + '/' + module['name'] + ': ' + result.stderr[:200])
verified = subprocess.run([verifier, prefix.with_suffix('.bc')], text=True, capture_output=True, timeout=8)
require(verified.returncode == 0, 'LLVM original-triple module verification failed: ' + module['library'] + '/' + module['name'] + ': ' + verified.stderr[:200])
text = prefix.with_suffix('.ll').read_text()
triple = re.search(r'^target triple = "([^"]+)"$', text, re.M)
require(triple and triple[1] == 'air64-apple-ios17.7.0', 'Actual original AIR target triple')
stages = [stage for stage in ('vertex', 'fragment', 'kernel', 'visible') if re.search(r'^!air\.' + stage + r' = ', text, re.M)]
require(len(stages) == 1, 'One actual original function-kind metadata record: ' + module['library'] + '/' + module['name'])
# This target's TYPE interpretation is checked against decoded AIR metadata.
require(module['type'] == {'vertex': 0, 'fragment': 1, 'kernel': 2, 'visible': 4}[stages[0]], 'Actual TYPE agrees with original AIR stage metadata')
definitions = re.findall(r'^define .*?@(?:"([^"]+)"|([^\s(]+))\(', text, re.M)
require(module['name'] in {a or b for a, b in definitions}, 'Original exported function is defined by its own decoded module')
module.update(triple=triple[1], stage=stages[0], definitions=len(definitions), llvm_module_verified=True,
air_intrinsics=sorted(set(re.findall(r'@(?:"(air\.[^"]+)"|(air\.[^\s(]+))\(', text))))
module['air_intrinsics'] = sorted({a or b for a, b in module['air_intrinsics']})
return module
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
modules = list(pool.map(decode, jobs))
report = dict(inputs=inputs, llvm_dis_version=version,
llvm_dis_sha256=hashlib.sha256(LLVM_DIS.read_bytes()).hexdigest(),
modules=modules, stages=dict(Counter(m['stage'] for m in modules)),
intrinsics=sorted({n for m in modules for n in m['air_intrinsics']}),
original_files_modified=False, host_bitcode_decode_only=True,
llvm_original_triple_module_verification=True,
llvm_library_sha256=hashlib.sha256((LLVM_ROOT / 'lib/libLLVM.dylib').read_bytes()).hexdigest(),
apple_bundle_signature_runtime_acceptance_qualified=False,
adreno_code_generated=False, shader_execution_qualified=False, metal_api_qualified=False)
(OUT / 'target-inputs.json').write_text(json.dumps(report, indent=2) + '\n')
print('PASS:', len(modules), 'original UI AIR modules decoded/verified with real triples/function-kind/export metadata and complete module hashes')
print('Stages:', report['stages'], '; distinct AIR intrinsics:', len(report['intrinsics']))
print('Host input qualification only: no Adreno shader generation/execution, Metal API or UI qualification')