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.
193 lines
9.3 KiB
Python
Executable File
193 lines
9.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: MIT
|
|
"""Acceptance/refusal tests for the guest/host boundary, without hardware."""
|
|
from dataclasses import replace
|
|
import hashlib
|
|
import contextlib
|
|
import io
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / 'probes'))
|
|
from bootcontract import Framebuffer, HostRuntime, PhysicalRange, checked_ranges
|
|
from guestprofile import J700_26A428, get_profile
|
|
import xnuboot as xb
|
|
import run
|
|
|
|
|
|
class BootContractTests(unittest.TestCase):
|
|
def test_queue_pointer_reproduces_and_rejects_live_fault(self):
|
|
old_va = 0xfffffed0019ed3c0
|
|
packed = (old_va >> 6) & 0xffffffff
|
|
self.assertEqual(packed, 0x40067b4f)
|
|
self.assertEqual(0xfffffe0000000000 | (packed << 6), 0xfffffe10019ed3c0)
|
|
with self.assertRaisesRegex(ValueError, 'reconstructs'):
|
|
J700_26A428.validate_queue_range(old_va, 64)
|
|
J700_26A428.validate_queue_range(J700_26A428.early_heap_va, xb.VSPACE_WALK_SPAN)
|
|
|
|
def test_queue_pointer_range_cannot_cross_its_encoding_limit(self):
|
|
end = J700_26A428.queue_pointer_base + (1 << 38)
|
|
J700_26A428.validate_queue_range(end - 64, 64)
|
|
for base, size in ((end - 64, 65), (end, 64), (end - 63, 64),
|
|
(J700_26A428.early_heap_va, 1 << 40)):
|
|
with self.subTest(base=base, size=size), self.assertRaises(ValueError):
|
|
J700_26A428.validate_queue_range(base, size)
|
|
|
|
def test_queue_pointer_format_is_profile_specific(self):
|
|
narrow = replace(J700_26A428, queue_pointer_bits=24)
|
|
with self.assertRaisesRegex(ValueError, 'reconstructs'):
|
|
narrow.validate_queue_range(J700_26A428.early_heap_va, 64)
|
|
with self.assertRaisesRegex(ValueError, 'overlaps'):
|
|
replace(J700_26A428, queue_pointer_base=J700_26A428.queue_pointer_base + 64).validate_queue_range(0, 64)
|
|
|
|
def host_info(self):
|
|
return dict(current_el=2, timer_hz=19200000,
|
|
heap_base=0x80000000, heap_size=0x4000000,
|
|
fb_base=0xf9a20000, fb_size=2880 * 1800 * 4,
|
|
fb_width=2880, fb_height=1800, fb_stride=2880, fb_format=1)
|
|
|
|
def test_host_timer_is_live_not_chip_constant(self):
|
|
info = self.host_info()
|
|
for hz in (19200000, 24000000, 62500000):
|
|
info['timer_hz'] = hz
|
|
self.assertEqual(HostRuntime.from_q1n1(info).timer_hz, hz)
|
|
|
|
def test_invalid_host_is_rejected(self):
|
|
for changes in (dict(current_el=1), dict(timer_hz=0),
|
|
dict(heap_size=0x500000), dict(timer_hz=1 << 32)):
|
|
with self.subTest(changes=changes), self.assertRaises(ValueError):
|
|
HostRuntime.from_q1n1(self.host_info() | changes)
|
|
|
|
def test_monitor_arena_does_not_require_heap_storage(self):
|
|
host = HostRuntime.from_q1n1(self.host_info() | dict(heap_size=0x501000))
|
|
self.assertEqual(host.heap.size, 0x501000)
|
|
|
|
def test_framebuffer_geometry_is_measured(self):
|
|
host = HostRuntime.from_q1n1(self.host_info(), with_framebuffer=True)
|
|
self.assertEqual(host.framebuffer.physical_range(),
|
|
PhysicalRange(0xf9a20000, 0x13c8000, 'panel'))
|
|
self.assertEqual(host.framebuffer.stride, 2880 * 4)
|
|
|
|
def test_invalid_framebuffer_is_rejected(self):
|
|
for changes in (dict(fb_size=16), dict(fb_stride=10), dict(fb_base=0),
|
|
dict(fb_format=2), dict(fb_height=0),
|
|
dict(fb_base=0x80000000), dict(fb_base=1 << 36)):
|
|
with self.subTest(changes=changes), self.assertRaises(ValueError):
|
|
HostRuntime.from_q1n1(self.host_info() | changes, with_framebuffer=True)
|
|
|
|
def test_unaligned_pixel_base_includes_last_page(self):
|
|
fb = Framebuffer(0x10003ffc, 1, 2, 4, 32)
|
|
self.assertEqual(fb.physical_range(), PhysicalRange(0x10000000, 0x8000, 'panel'))
|
|
|
|
def test_holes_do_not_become_owned_memory(self):
|
|
ranges = [PhysicalRange(0x10000, 0x4000, 'RAM'),
|
|
PhysicalRange(0x20000, 0x4000, 'panel')]
|
|
chunks, size = xb.build_framemap(0, 0x10000, 0x24000, 0xb, ranges)
|
|
self.assertEqual(chunks[0][2::16], bytes([0xb, 0, 0, 0, 0xb]))
|
|
self.assertEqual(size, 5 * 16)
|
|
|
|
def test_malformed_frame_maps_are_refused(self):
|
|
for low, high, kind in ((0, 1, 11), (0x4000, 0x4000, 11),
|
|
(0, 0x4000, 256)):
|
|
with self.subTest(low=low, high=high, kind=kind), self.assertRaises(ValueError):
|
|
xb.build_framemap(0, low, high, kind)
|
|
|
|
def test_boot_arguments_are_not_silently_truncated(self):
|
|
for line in ('x' * xb.BOOT_LINE_LENGTH, 'serial=2\0serial=3'):
|
|
with self.subTest(line=line[:20]), self.assertRaises(ValueError):
|
|
xb.boot_args(0, 0, 0, 0, 0, 0, line)
|
|
|
|
def test_overlapping_and_out_of_range_resources_are_refused(self):
|
|
ram = PhysicalRange(0x10000, 0x8000, 'RAM')
|
|
for region in (PhysicalRange(0x14000, 0x4000, 'panel'),
|
|
PhysicalRange(1 << 36, 0x4000, 'panel'),
|
|
PhysicalRange(0x20001, 0x4000, 'panel')):
|
|
with self.subTest(region=region), self.assertRaises(ValueError):
|
|
checked_ranges([ram, region])
|
|
|
|
def test_arbitrary_profile_name_is_not_a_fallback(self):
|
|
with self.assertRaisesRegex(ValueError, 'unsupported guest profile'):
|
|
get_profile('mac17j')
|
|
self.assertIs(get_profile('j700ap-26A428'), J700_26A428)
|
|
|
|
def test_hashes_follow_the_actual_selected_image(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
firmware = []
|
|
for name, _ in J700_26A428.firmware:
|
|
body = name.encode()
|
|
(root / name).write_bytes(body)
|
|
firmware.append((name, hashlib.sha256(body).hexdigest()))
|
|
profile = replace(J700_26A428, firmware=tuple(firmware))
|
|
self.assertEqual(profile.validate(profile.hashes(root)), [])
|
|
alternate = root / 'other-kernelcache'
|
|
alternate.write_bytes(b'a different binary')
|
|
errors = profile.validate(profile.hashes(root, alternate))
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn('kernelcache.release.mac17p: sha256', errors[0])
|
|
|
|
def test_build_cannot_bypass_identity_check(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
path = Path(directory) / 'kernelcache'
|
|
path.write_bytes(b'not the audited kernel')
|
|
with self.assertRaisesRegex(ValueError, 'firmware does not match'):
|
|
xb.build(path)
|
|
|
|
def test_firmware_directory_selection_is_consistent(self):
|
|
try:
|
|
xb.configure_guest(directory='/tmp/audited-bundle')
|
|
self.assertEqual(xb.DEFAULT_KERNELCACHE.parent, xb.FIRMWARE_DIR)
|
|
self.assertEqual(xb.DEFAULT_DEVICETREE.parent, xb.FIRMWARE_DIR)
|
|
self.assertEqual(xb.MONITOR_IMAGES, J700_26A428.monitors)
|
|
self.assertEqual(xb.XNU_VBAR, J700_26A428.vbar)
|
|
finally:
|
|
xb.configure_guest()
|
|
|
|
def test_budget_rejects_panel_that_needs_unimplemented_map_table(self):
|
|
old = xb.FRAMEBUFFER
|
|
try:
|
|
dt = xb.build_devicetree()
|
|
xb.FRAMEBUFFER = (0xf9a20000, 2732, 1896, 2732 * 4, 32)
|
|
demand, capacity = xb.early_mapping_budget(dt)
|
|
self.assertLess(demand, capacity)
|
|
self.assertEqual(capacity, 2048)
|
|
xb.FRAMEBUFFER = (0xf9a20000, 4096, 4096, 4096 * 4, 32)
|
|
with self.assertRaisesRegex(ValueError, 'before MAP_TABLE'):
|
|
xb.early_mapping_budget(dt)
|
|
finally:
|
|
xb.FRAMEBUFFER = old
|
|
|
|
def test_video_witness_distinguishes_mapping_from_acquisition(self):
|
|
witness = J700_26A428.video_console
|
|
fields = {witness.vinfo: 1800, witness.vinfo + 4: 2880,
|
|
witness.vinfo + 8: 32, witness.vinfo + 12: 11520,
|
|
witness.vinfo + 0x10: 0xfffffed000014000,
|
|
witness.vinfo + 0x40: 0xf9a20000,
|
|
witness.graphics_mode: 0, witness.acquired: 0}
|
|
built = dict(video_console_witness=witness,
|
|
framebuffer=(0xf9a20000, 2880, 1800, 11520, 32))
|
|
output = io.StringIO()
|
|
with contextlib.redirect_stdout(output):
|
|
state = run.video_console_state(lambda va, n=8: fields[va], built)
|
|
self.assertTrue(state['geometry_matches'])
|
|
self.assertEqual(state['acquired'], 0)
|
|
self.assertIn('has not acquired', output.getvalue())
|
|
fields[witness.vinfo + 4] = 1920
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertFalse(run.video_console_state(lambda va, n=8: fields[va], built)['geometry_matches'])
|
|
|
|
def test_video_witness_never_borrows_another_guests_addresses(self):
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertIsNone(run.video_console_state(
|
|
lambda *args: self.fail('unprofiled guest memory was read'), {}))
|
|
with self.assertRaisesRegex(RuntimeError, 'unmapped'):
|
|
run.video_console_state(lambda *args: (_ for _ in ()).throw(RuntimeError('unmapped')),
|
|
dict(video_console_witness=J700_26A428.video_console))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|