From cb5a6790e8c9d45fa1f74e11a9b876d90dc59ba7 Mon Sep 17 00:00:00 2001 From: Dj <43033058+DjDeveloperr@users.noreply.github.com> Date: Mon, 26 Oct 2026 02:34:16 -0400 Subject: [PATCH] arm64: apple: Add checked T6050 APIF guest migration Add checked APIF guest-device migration and configuration auditing. Preserve retained firmware resources and reject handoffs that lack the required provider, mapping, or kernel configuration metadata. Source snapshot: t6050-gpu ee68abb105200e99636a5edfc647a5cfc1d89954 Integration base: 9cbdd0734d9e186c5f5f5949a21b909d10c2571b Signed-off-by: Dj <43033058+DjDeveloperr@users.noreply.github.com> --- tools/aurora-apif/README.rst | 92 ++++++ tools/aurora-apif/check_guest_config.py | 125 +++++++ tools/aurora-apif/prepare_guest.py | 328 +++++++++++++++++++ tools/testing/selftests/aurora-apif/guest.py | 146 +++++++++ 4 files changed, 691 insertions(+) create mode 100644 tools/aurora-apif/README.rst create mode 100755 tools/aurora-apif/check_guest_config.py create mode 100755 tools/aurora-apif/prepare_guest.py create mode 100755 tools/testing/selftests/aurora-apif/guest.py diff --git a/tools/aurora-apif/README.rst b/tools/aurora-apif/README.rst new file mode 100644 index 0000000000000..24ce06f9430ba --- /dev/null +++ b/tools/aurora-apif/README.rst @@ -0,0 +1,92 @@ +.. SPDX-License-Identifier: GPL-2.0-only + +Native APIF guest preparation +============================= + +Apply ``arch/arm64/configs/aurora-apif.config`` after the existing Asahi +fragment, then run ``olddefconfig``. It enables the native transport, DART, +SART, ANS, PCIe and PMP providers. Both SART components must be built in: +``APPLE_SART_APIF`` requires ``APPLE_SART=y``. These drivers use the native +Linux build workflow and need no external build VM or wrapper. + +The boot producer owns physical addresses, grants, calibration and the stopped +handoff. Linux does not reconstruct them from a machine name. A generated +version-1 description can be migrated on a copy with Python libfdt:: + + tools/aurora-apif/prepare_guest.py producer.dtb native-apif.dtb \ + --pcie-layout t6050-gen3-x1 + +The output must be a new file. Validation failure writes nothing. This tool +does not install, boot or modify hardware. Use the result only with the matching +Aura version-2 monitor at CPU IPA ``0x61ff0000``. The version-1 transport remains +in the tree for other consumers; its sixteen alias pages do not overlap v2. + +DART unit IDs, stream counts, native force masks, DMA apertures and retained +SIDs are preserved. Provider phandles remain stable, legacy unit-address names +are removed, and standard aliases/symbol paths are repaired. The final-page +allocation guard formerly hidden in the PMP consumer match becomes explicit +provider metadata. PMP status is preserved unless ``--enable-pmp`` is supplied. +Unrelated device nodes and opaque data are preserved. + +ANS requires four distinct 16 KiB ``no-map`` queue reservations with the +native addresses already pinned by firmware, in ``admin-sq``, ``admin-cq``, +``io-sq``, ``io-cq`` order. The producer must declare coherent memory and +``asahi,rtkit-quiesced`` after actually stopping both RTKit peers and controller +DMA. The converter refuses live or incomplete handoffs; it never allocates +replacement pages or invents ownership. The native driver independently +checks translations and requires an identity DMA aperture. + +A preboot PCIe cycle additionally requires the explicitly selected T6050 +Gen3 x1 layout, five named resources, stopped ``apple,preinitialized`` state +and existing ``apple,rid-to-sid`` tuples matching ``iommu-map``. MSI address +and AIC range remain producer-supplied. There is no automatic board/layout +inference or N1 radio driver activation in this migration. + +CPU resource overlap is checked for root and identity CPU buses. Translation +through nonempty bus ranges and PCI BAR encodings remains the producer's +responsibility. The tool does not prove firmware state, native permissions, +thermal policy or hardware qualification. Old live-session m5 trees lacking +queue reservations cannot be converted into a safe stopped handoff merely by +setting a boolean. Update the producer first, retain its original DTB, and +qualify boot, storage durability, MSI, USB DMA, PMP and teardown separately. + +The matched T6050 Abbey RTC is migrated to ``apple,abbey-pmu-rtc`` only +when its five named cells describe the counter, offset, alarm, control +and interrupt mask. This leaves the separate Baku RTC binding unchanged. + +T6050 right-port USB DART must name its ATC2_USB power domain independently +of the DWC3 consumer. Its DART gate has no local power-state register and +requires the shared controller parent before native OPEN reads DART registers. +The migration rejects an enabled T6050 USB controller using native DART16 +unless both nodes carry the producer-supplied shared power-domain dependency. +It preserves that dependency on the renamed APIF IOMMU node; platform PM +attachment therefore precedes IOMMU probe and native access. This does not +replace physical hotplug/DMA qualification. + +The T6050 GPU keeps its separate compatibility-operation page at +``0x61f3c000`` with register name ``x1n1-uat``. The generic APIF v2 page +does not replace that GPU ABI. Migration preserves the GPU apertures, +CPU release offsets, guest-device mask and QSPI/NVRAM description. +It accepts the legacy ANS ``nvmmu`` resource only to remove that mediated +window after validating the stopped handoff. The explicitly selected +T6050 PCIe layout normalizes its historical ``config,port,phy,intr2axi,perst`` +resource order to ``ecam,port,phy,perst,intr2axi`` without changing extents. + +Kernel configuration check +-------------------------- + +T6050 boot NOR uses ``CONFIG_SPI_APPLE_QSPI``, not ``CONFIG_SPI_APPLE``. +The APIF fragment explicitly enables QSPIMC, SPI memory, MTD, SPI-NOR and +fixed-partition support. Apply it before ``olddefconfig``, then check the +resolved configuration against the enabled producer nodes:: + + tools/aurora-apif/check_guest_config.py native-apif.dtb build/.config + +The checker covers the known M5 guest peripherals and their required build +options. It follows disabled parent nodes, accepts built-in or modular +drivers by default, and reports unmatched compatibles as unaudited rather +than unsupported. ``--built-in`` requires every matched option to be ``y``; +otherwise the corresponding modules must be supplied by the deployment. +This checks configuration coverage, not hardware behavior or module contents. +Migration can also take ``--kernel-config build/.config``; missing known +driver options then reject the migration before writing its output. diff --git a/tools/aurora-apif/check_guest_config.py b/tools/aurora-apif/check_guest_config.py new file mode 100755 index 0000000000000..763baab2ae4df --- /dev/null +++ b/tools/aurora-apif/check_guest_config.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Check enabled guest peripheral drivers against a resolved kernel config.""" +import argparse +import json +from pathlib import Path +import re +import libfdt + +# This is a reviewed mapping of guest consumers, not a replacement for Kconfig. +# An unlisted compatible is reported as unaudited, never as unsupported. +DRIVERS = { + "aurora,apif-mmio": ("AURORA_APIF_MMIO",), + "apple,dart-apif": ("APPLE_DART_APIF", "AURORA_APIF_MMIO"), + "apple,sart-apif": ("APPLE_SART", "APPLE_SART_APIF", "AURORA_APIF_MMIO"), + "apple,nvme-apif": ("NVME_APPLE", "AURORA_APIF_MMIO"), + "apple,apif-pcie": ("PCIE_APPLE_APIF", "PCI", "PCI_MSI"), + "apple,t8140-qspi": ("SPI", "SPI_MEM", "SPI_APPLE_QSPI"), + "apple,t6050-qspi": ("SPI", "SPI_MEM", "SPI_APPLE_QSPI"), + "jedec,spi-nor": ("MTD", "MTD_SPI_NOR", "SPI_MEM"), + "fixed-partitions": ("MTD", "MTD_OF_PARTS"), + "fixed-clock": ("COMMON_CLK",), + "syscon": ("MFD_SYSCON",), + "apple,asc-mailbox-v4": ("APPLE_MAILBOX",), + "apple,t6050-ascwrap-v6": ("APPLE_MAILBOX",), + "apple,t6050-x1n1-ascwrap-v6": ("APPLE_MAILBOX",), + "apple,t6050-pmp-v2": ("APPLE_PMP",), + "apple,t6050-dwc3": ("USB_DWC3", "USB_DWC3_APPLE", "USB_XHCI_HCD", "USB_XHCI_PLATFORM", "USB_ROLE_SWITCH"), + "apple,t6050-spmi": ("SPMI", "SPMI_APPLE"), + "apple,sn201202x": ("TYPEC", "TYPEC_SN201202X", "TYPEC_TPS6598X_CORE"), + "apple,t8142-display-crossbar": ("MULTIPLEXER", "MUX_APPLE_DPXBAR"), + "apple,t6050-combo-phy": ("PHY_APPLE_T6050_DP", "TYPEC", "APPLE_RTKIT", "RESET_CONTROLLER"), + "apple,t6050-dp-phy": ("PHY_APPLE_T6050_DP",), + "apple,t6050-dcp": ("DRM", "DRM_APPLE", "APPLE_RTKIT"), + "apple,t6050-dcpext": ("DRM", "DRM_APPLE", "APPLE_RTKIT"), + "apple,display-subsystem": ("DRM_APPLE",), + "apple,pinctrl": ("PINCTRL", "PINCTRL_APPLE_GPIO"), + "apple,pmgr-pwrstate": ("APPLE_PMGR_PWRSTATE", "PM_GENERIC_DOMAINS"), + "apple,t8103-pmgr-pwrstate": ("APPLE_PMGR_PWRSTATE", "PM_GENERIC_DOMAINS"), + "apple,t8103-pmgr": ("APPLE_PMGR_MISC",), + "apple,dockchannel": ("APPLE_DOCKCHANNEL",), + "apple,dockchannel-hid": ("HID_DOCKCHANNEL",), + "apple,rtk-helper-asc4": ("APPLE_RTKIT_HELPER",), + "apple,s5l-fpwm": ("PWM", "PWM_APPLE"), + "pwm-leds": ("LEDS_CLASS", "LEDS_PWM", "PWM"), + "apple,wdt": ("WATCHDOG", "APPLE_WATCHDOG"), + "apple,t6050-cluster-cpufreq": ("CPU_FREQ", "ARM_APPLE_SOC_CPUFREQ"), + "apple,t6050-smc": ("MFD_MACSMC",), + "apple,smc-pcio-gpio": ("GPIO_MACSMC",), + "apple,smc-hwmon": ("SENSORS_MACSMC_HWMON",), + "apple,t8122-aic3": ("APPLE_AIC",), + "arm,armv8-timer": ("ARM_ARCH_TIMER",), + "apple,s5l-uart": ("SERIAL_SAMSUNG", "SERIAL_SAMSUNG_CONSOLE"), + "pci106b,1903": ("BT", "BT_HCICENTAURI", "APPLE_CENTAURI"), + "pci106b,1902": ("APPLE_CENTAURI_WIFI", "APPLE_CENTAURI"), + "gpu,t6050": ("DRM_ASAHI", "AURORA_APIF_GPU"), + "apple,agx-t6050": ("DRM_ASAHI", "AURORA_APIF_GPU"), +} +# Declarative/container nodes whose compatible does not imply another driver. +METADATA = {"fixed-clock", "simple-bus", "simple-mfd", "syscon", "operating-points-v2", + "arm,armv8", "framebuffer", "usb-c-connector", "apple,panel-mini-led"} + + +def parse_config(text): + return dict(re.findall(r"^(CONFIG_[A-Za-z0-9_]+)=(.+)$", text, re.M)) + + +def strings(tree, node, name): + value = tree.getprop(node, name, quiet=(libfdt.NOTFOUND,)) + return [] if isinstance(value, int) else bytes(value).rstrip(b"\0").decode().split("\0") + + +def audit(blob, config, builtin=False): + tree = libfdt.Fdt(blob) + values = parse_config(config) + result = {"matched": [], "missing": [], "unaudited": []} + accepted = ("y",) if builtin else ("y", "m") + + def visit(node, parent_enabled=True): + status = strings(tree, node, "status") + enabled = parent_enabled and (not status or status in (["okay"], ["ok"])) + if enabled: + compatibles = strings(tree, node, "compatible") + matched = next((compatible for compatible in compatibles if compatible in DRIVERS), None) + path = tree.get_path(node) + if matched: + symbols = {"CONFIG_" + symbol: values.get("CONFIG_" + symbol, "n") + for symbol in DRIVERS[matched]} + result["matched"].append({"node": path, "compatible": matched, "symbols": symbols}) + for symbol, value in symbols.items(): + if value not in accepted: + result["missing"].append({"node": path, "symbol": symbol, "value": value}) + elif node and compatibles and not any(c in METADATA for c in compatibles): + result["unaudited"].append({"node": path, "compatible": compatibles}) + child = tree.first_subnode(node, quiet=(libfdt.NOTFOUND,)) + while child >= 0: + visit(child, enabled) + child = tree.next_subnode(child, quiet=(libfdt.NOTFOUND,)) + + visit(0) + return result + + +def validate(blob, config, builtin=False): + result = audit(blob, config, builtin) + if result["missing"]: + missing = "; ".join(f"{entry['node']}: {entry['symbol']}={entry['value']}" + for entry in result["missing"]) + raise ValueError("enabled guest peripherals lack kernel support: " + missing) + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("dtb", type=Path) + parser.add_argument("config", type=Path) + parser.add_argument("--built-in", action="store_true", help="require y rather than accepting loadable modules") + args = parser.parse_args() + result = audit(args.dtb.read_bytes(), args.config.read_text(), args.built_in) + print(json.dumps(result, indent=2)) + return bool(result["missing"]) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/aurora-apif/prepare_guest.py b/tools/aurora-apif/prepare_guest.py new file mode 100755 index 0000000000000..c61201ffd809a --- /dev/null +++ b/tools/aurora-apif/prepare_guest.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Migrate a boot-producer DTB to native APIF; never infer hardware ownership.""" +import argparse +from pathlib import Path +import struct + +import libfdt + +APIF_IPA = 0x61FF0000 +PAGE_SIZE = 0x4000 +LEGACY_DART = "apple,dart-x1n1" +LEGACY_SART = "apple,sart-x1n1" +LEGACY_NVME = "apple,t6050-nvme-sptm" +LEGACY_PCIE = "apple,x1n1-pcie" + + +def property_bytes(tree, path, name): + value = tree.getprop(tree.path_offset(path), name, quiet=(libfdt.NOTFOUND,)) + return None if isinstance(value, int) else bytes(value) + + +def strings(tree, path, name): + data = property_bytes(tree, path, name) + if data is None: + return [] + if not data.endswith(b"\0"): + raise ValueError(f"{path}: malformed {name}") + return data[:-1].decode("ascii").split("\0") + + +def cells(tree, path, name): + data = property_bytes(tree, path, name) + if data is None or len(data) % 4: + raise ValueError(f"{path}: missing or malformed {name}") + return list(struct.unpack(f">{len(data) // 4}I", data)) + + +def number(tree, path, name): + values = cells(tree, path, name) + if len(values) != 1: + raise ValueError(f"{path}: {name} must be one cell") + return values[0] + + +def paths(tree): + offset, depth = 0, 0 + while offset >= 0 and depth >= 0: + yield tree.get_path(offset) + offset, depth = tree.next_node(offset, depth, quiet=(libfdt.NOTFOUND,)) + + +def put(tree, path, name, value): + tree.setprop(tree.path_offset(path), name, value) + + +def put_cells(tree, path, name, values): + put(tree, path, name, struct.pack(f">{len(values)}I", *values)) + + +def put_strings(tree, path, name, values): + put(tree, path, name, b"\0".join(v.encode("ascii") for v in values) + b"\0") + + +def delete(tree, path, name): + tree.delprop(tree.path_offset(path), name, quiet=(libfdt.NOTFOUND,)) + + +def reservation(tree, phandle): + offset = tree.node_offset_by_phandle(phandle) + path = tree.get_path(offset) + parent = path.rsplit("/", 1)[0] + if parent != "/reserved-memory" or property_bytes(tree, parent, "ranges") != b"" or property_bytes(tree, path, "no-map") is None: + raise ValueError(f"{path}: queue page must be no-map reserved memory") + ac, sc = number(tree, parent, "#address-cells"), number(tree, parent, "#size-cells") + if ac not in (1, 2) or sc not in (1, 2): + raise ValueError("unsupported reserved-memory cell widths") + reg = cells(tree, path, "reg") + if len(reg) != ac + sc: + raise ValueError(f"{path}: queue page must have exactly one extent") + address = int.from_bytes(struct.pack(f">{ac}I", *reg[:ac]), "big") + size = int.from_bytes(struct.pack(f">{sc}I", *reg[ac:]), "big") + if address % PAGE_SIZE or size != PAGE_SIZE: + raise ValueError(f"{path}: queue page must be aligned and 16 KiB sized") + return address + + +def nvme_handoff(tree, path): + if property_bytes(tree, path, "asahi,rtkit-quiesced") is None: + raise ValueError(f"{path}: producer must quiesce RTKit before migration") + if property_bytes(tree, path, "dma-coherent") is None: + raise ValueError(f"{path}: coherent handoff required") + names = strings(tree, path, "memory-region-names") + if names != ["admin-sq", "admin-cq", "io-sq", "io-cq"]: + raise ValueError(f"{path}: four named pinned queue pages required") + handles = cells(tree, path, "memory-region") + if len(handles) != 4 or len(set(reservation(tree, h) for h in handles)) != 4: + raise ValueError(f"{path}: queue reservations must be distinct") + + +def usb_dart_power(tree, compatible): + """Identify USB DARTs after checking their register-clock dependency.""" + providers = set() + for path, matches in compatible.items(): + if "apple,t6050-dwc3" not in matches or strings(tree, path, "status") == ["disabled"]: + continue + iommu = cells(tree, path, "iommus") + if len(iommu) != 2: + raise ValueError(f"{path}: one USB DART stream required") + provider = tree.get_path(tree.node_offset_by_phandle(iommu[0])) + if LEGACY_DART not in strings(tree, provider, "compatible"): + continue + # T6050's right USB DART gate has no local power state: its native + # OPEN touches registers clocked by the controller's ATC2_USB parent. + if number(tree, provider, "apple,x1n1-dart-id") != 16: + continue + domains = property_bytes(tree, provider, "power-domains") + if domains is None or domains != property_bytes(tree, path, "power-domains"): + raise ValueError(f"{provider}: T6050 USB DART requires the producer's ATC2_USB power-domain dependency before OPEN") + providers.add(provider) + return providers + + +def pci_layout(tree, path, layout): + if property_bytes(tree, path, "apple,preinitialized") is None: + raise ValueError(f"{path}: stopped preinitialized producer handoff required") + if layout != "t6050-gen3-x1": + raise ValueError(f"{path}: explicitly select the producer's PCIe layout") + names = strings(tree, path, "reg-names") + if names == ["config", "port", "phy", "intr2axi", "perst"]: + parent = path.rsplit("/", 1)[0] or "/" + width = number(tree, parent, "#address-cells") + number(tree, parent, "#size-cells") + registers = cells(tree, path, "reg") + if len(registers) != 5 * width: + raise ValueError(f"{path}: incomplete legacy PCIe register extents") + put_cells(tree, path, "reg", sum([registers[i*width:(i+1)*width] + for i in [0, 1, 2, 4, 3]], [])) + put_strings(tree, path, "reg-names", ["ecam", "port", "phy", "perst", "intr2axi"]) + if strings(tree, path, "reg-names") != ["ecam", "port", "phy", "perst", "intr2axi"]: + raise ValueError(f"{path}: matched PCIe register resources required") + if number(tree, path, "max-link-speed") != 3 or number(tree, path, "num-lanes") != 1: + raise ValueError(f"{path}: only the matched Gen3 x1 layout supports port cycling") + routes = cells(tree, path, "apple,rid-to-sid") + if not routes or len(routes) % 3 or len(routes) > 45: + raise ValueError(f"{path}: explicit RID-to-SID tuples required") + return ["apple,t6050-pcie-apif", "apple,apif-pcie"] + + +def migrate(blob, pcie_layout=None, enable_pmp=False): + tree = libfdt.Fdt(bytearray(blob)) + tree.resize(len(blob) * 2 + 16384) + inventory = list(paths(tree)) + compatible = {p: strings(tree, p, "compatible") for p in inventory} + usb_darts = usb_dart_power(tree, compatible) + for path, matches in compatible.items(): + if "apple,t6050-x1n1-ascwrap-v6" in matches: + put_strings(tree, path, "compatible", ["apple,t6050-ascwrap-v6"]) + targets = [p for p, c in compatible.items() if any(v in c for v in + [LEGACY_DART, LEGACY_SART, LEGACY_NVME, LEGACY_PCIE])] + # Reject direct CPU-space collisions; buses without ranges are not CPU + # address spaces. PCI child BAR encodings remain owned by the producer. + for path in inventory: + if path == "/" or property_bytes(tree, path, "reg") is None: + continue + parent = path.rsplit("/", 1)[0] or "/" + pa, ps = property_bytes(tree, parent, "#address-cells"), property_bytes(tree, parent, "#size-cells") + if pa is None or ps is None: + continue + ca, cs = number(tree, parent, "#address-cells"), number(tree, parent, "#size-cells") + if ca not in (1, 2) or cs not in (1, 2): + continue + # Identity CPU buses are common in generated guest descriptions. + bus = parent + while bus != "/" and property_bytes(tree, bus, "ranges") == b"": + bus = bus.rsplit("/", 1)[0] or "/" + if bus != "/": + continue + reg = cells(tree, path, "reg") + if len(reg) % (ca + cs): + raise ValueError(f"{path}: malformed CPU register extents") + for i in range(0, len(reg), ca + cs): + address = int.from_bytes(struct.pack(f">{ca}I", *reg[i:i+ca]), "big") + size = int.from_bytes(struct.pack(f">{cs}I", *reg[i+ca:i+ca+cs]), "big") + if size and address < APIF_IPA + PAGE_SIZE and APIF_IPA < address + size: + raise ValueError(f"{path}: native APIF page overlaps a CPU resource") + if not targets: + raise ValueError("no version-1 providers or consumers to migrate") + # The native page uses the root's CPU physical address space. + ac, sc = number(tree, "/", "#address-cells"), number(tree, "/", "#size-cells") + if ac not in (1, 2) or sc not in (1, 2): + raise ValueError("unsupported root address/size cell widths") + handles = {} + for path in inventory: + for name in ["phandle", "linux,phandle"]: + data = property_bytes(tree, path, name) + if data is not None: + handle = number(tree, path, name) + if not handle or handle == 0xFFFFFFFF or (handle in handles and handles[handle] != path): + raise ValueError("invalid or duplicate phandle") + handles[handle] = path + handle = max(handles, default=0) + 1 + if handle >= 0xFFFFFFFF: + raise ValueError("no available phandle") + if any("aurora,apif-mmio" in c for c in compatible.values()): + raise ValueError("tree already contains native APIF; do not migrate it twice") + apif = "/apif@61ff0000" + tree.add_subnode(0, apif[1:]) + put_strings(tree, apif, "compatible", ["aurora,apif-mmio"]) + put_cells(tree, apif, "phandle", [handle]) + put(tree, apif, "reg", APIF_IPA.to_bytes(4 * ac, "big") + PAGE_SIZE.to_bytes(4 * sc, "big")) + renamed = {} + for path in targets: + c = compatible[path] + if LEGACY_DART in c: + unit = number(tree, path, "apple,x1n1-dart-id") + if unit > 255: + raise ValueError(f"{path}: invalid native DART unit") + put_strings(tree, path, "compatible", ["apple,dart-apif"]) + for old, new in [("dart-id", "dart-id"), ("num-streams", "num-streams"), + ("iova-force", "iova-force"), ("retained-sids", "retained-sids")]: + data = property_bytes(tree, path, "apple,x1n1-" + old) + if data is not None: + put(tree, path, "apple,apif-" + new, data) + delete(tree, path, "apple,x1n1-" + old) + delete(tree, path, "apple,x1n1-selftest") + count = number(tree, path, "apple,apif-num-streams") if property_bytes(tree, path, "apple,apif-num-streams") else 256 + if not 1 <= count <= 256: + raise ValueError(f"{path}: invalid stream count") + retained = cells(tree, path, "apple,apif-retained-sids") if property_bytes(tree, path, "apple,apif-retained-sids") else [] + if len(retained) != len(set(retained)) or any(sid >= count for sid in retained): + raise ValueError(f"{path}: invalid retained streams") + # Native MAP requires both start and exclusive end strictly + # inside this USB stream's aperture. Preserve its final granule. + if path in usb_darts: + put(tree, path, "apple,apif-reserve-last-page", b"") + renamed[path] = path.rsplit("/", 1)[0] + f"/iommu-{unit:x}" + delete(tree, path, "reg") + elif LEGACY_SART in c: + put_strings(tree, path, "compatible", ["apple,sart-apif"]) + renamed[path] = path.rsplit("/", 1)[0] + "/sart" + delete(tree, path, "reg") + elif LEGACY_NVME in c: + nvme_handoff(tree, path) + put_strings(tree, path, "compatible", ["apple,nvme-apif"]) + # Version-1 has a third transport window; native APIF is a phandle. + names = strings(tree, path, "reg-names") + reg = cells(tree, path, "reg") + parent = path.rsplit("/", 1)[0] or "/" + width = number(tree, parent, "#address-cells") + number(tree, parent, "#size-cells") + if names not in (["nvme", "ans", "sptm"], ["nvme", "ans", "nvmmu", "sptm"]) or len(reg) != len(names) * width: + raise ValueError(f"{path}: expected version-1 NVMe resources") + put_cells(tree, path, "reg", reg[:2 * width]) + put_strings(tree, path, "reg-names", ["nvme", "ans"]) + else: + put_strings(tree, path, "compatible", pci_layout(tree, path, pcie_layout)) + if LEGACY_PCIE not in c: + put_cells(tree, path, "aurora,apif", [handle]) + # Identify the legacy RTC layout only on matched T6050 guests. + if "apple,t6050" in compatible.get("/", []): + for path, c in compatible.items(): + if "apple,pmu-rtc" in c: + names = strings(tree, path, "nvmem-cell-names") + if names != ["counter", "rtc_offset", "alarm", "alarm_ctrl", "irq_mask"]: + raise ValueError(f"{path}: unknown PMU RTC cell layout") + put_strings(tree, path, "compatible", ["apple,abbey-pmu-rtc"]) + # The old driver applied a hidden guard to the matched PMP consumer. + # Record that existing ABI rule explicitly on its provider instead. + for path, c in compatible.items(): + if "apple,t6050-pmp-v2" not in c: + continue + iommu = cells(tree, path, "iommus") + if len(iommu) != 2 or iommu[0] not in handles: + raise ValueError(f"{path}: one described PMP DART stream required") + provider = handles[iommu[0]] + if strings(tree, provider, "compatible") != ["apple,dart-apif"]: + raise ValueError(f"{path}: PMP requires its APIF DART provider") + put(tree, provider, "apple,apif-reserve-last-page", b"") + if enable_pmp: + put_strings(tree, path, "status", ["okay"]) + # Retain every provider phandle, and repair standard path references. + if len(set(renamed.values())) != len(renamed): + raise ValueError("provider names would collide") + for old, new in renamed.items(): + if new in inventory and new != old: + raise ValueError(f"{new}: provider name collision") + tree.set_name(tree.path_offset(old), new.rsplit("/", 1)[1]) + for metadata in ["/aliases", "/__symbols__"]: + offset = tree.path_offset(metadata, quiet=(libfdt.NOTFOUND,)) + if offset < 0: + continue + props = [] + po = tree.first_property_offset(offset, quiet=(libfdt.NOTFOUND,)) + while po >= 0: + prop = tree.get_property_by_offset(po) + props.append((prop.name, bytes(prop))) + po = tree.next_property_offset(po, quiet=(libfdt.NOTFOUND,)) + for name, data in props: + for old, new in renamed.items(): + if data == old.encode() + b"\0": + put_strings(tree, metadata, name, [new]) + tree.pack() + return bytes(tree.as_bytearray()) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input", type=Path) + parser.add_argument("output", type=Path) + parser.add_argument("--pcie-layout", choices=["t6050-gen3-x1"]) + parser.add_argument("--enable-pmp", action="store_true") + parser.add_argument("--kernel-config", type=Path, + help="check enabled guest peripheral drivers before writing output") + args = parser.parse_args() + if args.input.resolve() == args.output.resolve() or args.output.exists(): + parser.error("output must be a new file; preserve the producer's original DTB") + try: + output = migrate(args.input.read_bytes(), args.pcie_layout, args.enable_pmp) + if args.kernel_config: + from check_guest_config import validate + validate(output, args.kernel_config.read_text()) + except (ValueError, libfdt.FdtException) as error: + parser.error(str(error)) + with args.output.open("xb") as stream: + stream.write(output) + + +if __name__ == "__main__": + main() diff --git a/tools/testing/selftests/aurora-apif/guest.py b/tools/testing/selftests/aurora-apif/guest.py new file mode 100755 index 0000000000000..bef29ebea12a2 --- /dev/null +++ b/tools/testing/selftests/aurora-apif/guest.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only +"""Check migration boundaries, metadata preservation and failed handoffs.""" +import importlib.util +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile + +ROOT = Path(__file__).resolve().parents[4] + + +def main(): + print("TAP version 13\n1..1", flush=True) + if not shutil.which("dtc"): + print("ok 1 - guest migration # SKIP dtc unavailable") + return 4 + try: + import libfdt + except ImportError: + print("ok 1 - guest migration # SKIP Python libfdt unavailable") + return 4 + spec = importlib.util.spec_from_file_location("prepare_guest", ROOT / "tools/aurora-apif/prepare_guest.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + source = r'''/dts-v1/; +/ { + #address-cells = <2>; #size-cells = <2>; + aliases { pmp-iommu = "/iommu@61f34000"; }; + reserved-memory { + #address-cells = <2>; #size-cells = <2>; ranges; + asq: queue@80000000 {reg=<0 0x80000000 0 0x4000>; no-map;}; + acq: queue@80004000 {reg=<0 0x80004000 0 0x4000>; no-map;}; + isq: queue@80008000 {reg=<0 0x80008000 0 0x4000>; no-map;}; + icq: queue@8000c000 {reg=<0 0x8000c000 0 0x4000>; no-map;}; + }; + dart: iommu@61f34000 { + compatible="apple,dart-x1n1"; reg=<0 0x61f34000 0 0x4000>; + #iommu-cells=<1>; apple,x1n1-dart-id=<12>; + apple,x1n1-num-streams=<16>; apple,x1n1-retained-sids=<0 1>; + apple,dma-range=<0x100 0 0x10 0>; + }; + sart: sart@61f04000 {compatible="apple,sart-x1n1"; reg=<0 0x61f04000 0 0x4000>;}; + nvme@60400000 { + compatible="apple,t6050-nvme-sptm"; + reg=<0 0x60400000 0 0x40000>,<0 0x61000000 0 0x4000>,<0 0x61f00000 0 0x4000>; + reg-names="nvme","ans","sptm"; dma-coherent; asahi,rtkit-quiesced; + memory-region=<&asq &acq &isq &icq>; + memory-region-names="admin-sq","admin-cq","io-sq","io-cq"; + apple,sart=<&sart>; + }; + pmp@300500000 {compatible="apple,t6050-pmp-v2"; iommus=<&dart 0>; status="disabled";}; + usb_power: power-domain {#power-domain-cells=<0>;}; + usb_dart: iommu@61f38000 { + compatible="apple,dart-x1n1"; reg=<0 0x61f38000 0 0x4000>; + #iommu-cells=<1>; apple,x1n1-dart-id=<16>; + power-domains=<&usb_power>; + }; + usb@8a000000 { + compatible="apple,t6050-dwc3"; reg=<0 0x8a000000 0 0x10000>; + iommus=<&usb_dart 0>; power-domains=<&usb_power>; + }; + gpu {compatible="example,opaque-gpu"; unchanged=[de ad be ef];}; + gpu@800000000 {compatible="gpu,t6050"; + reg=<8 0 0 0x3fdc000>,<0 0x61f3c000 0 0x4000>; + reg-names="sgx","x1n1-uat";}; + chosen {x1n1,guest-devices=<0x7ff>;}; + cpus {#address-cells=<1>; #size-cells=<0>; + cpu@1 {reg=<1>; cpu-release-addr=<0 0x4fffc008>;};}; + spi@62630000 {reg=<0 0x62630000 0 0x4000>; status="okay";}; +};''' + with tempfile.TemporaryDirectory(prefix="apif-guest-") as directory: + path = Path(directory) + (path / "input.dts").write_text(source) + subprocess.run(["dtc", "-q", "-I", "dts", "-O", "dtb", "-o", str(path / "input.dtb"), + str(path / "input.dts")], check=True) + blob = (path / "input.dtb").read_bytes() + before = libfdt.Fdt(bytearray(blob)) + after = libfdt.Fdt(bytearray(module.migrate(blob))) + assert module.strings(after, "/iommu-c", "compatible") == ["apple,dart-apif"] + for old, new in [("apple,x1n1-retained-sids", "apple,apif-retained-sids"), + ("apple,dma-range", "apple,dma-range"), ("phandle", "phandle")]: + assert module.property_bytes(before, "/iommu@61f34000", old) == module.property_bytes(after, "/iommu-c", new) + assert module.property_bytes(after, "/iommu-c", "apple,apif-reserve-last-page") == b"" + assert module.strings(after, "/aliases", "pmp-iommu") == ["/iommu-c"] + assert module.strings(after, "/pmp@300500000", "status") == ["disabled"] + assert module.property_bytes(after, "/gpu", "unchanged") == module.property_bytes(before, "/gpu", "unchanged") + assert len(module.cells(after, "/nvme@60400000", "reg")) == 8 + assert module.cells(after, "/apif@61ff0000", "reg") == [0, module.APIF_IPA, 0, module.PAGE_SIZE] + assert module.property_bytes(after, "/iommu-10", "power-domains") == module.property_bytes(before, "/usb@8a000000", "power-domains") + assert module.property_bytes(after, "/iommu-10", "apple,apif-reserve-last-page") == b"" + for path, properties in [("/gpu@800000000", ["reg", "reg-names", "compatible"]), + ("/chosen", ["x1n1,guest-devices"]), + ("/cpus/cpu@1", ["cpu-release-addr"]), + ("/spi@62630000", ["reg", "status"])]: + for prop in properties: + assert module.property_bytes(after, path, prop) == module.property_bytes(before, path, prop) + four = libfdt.Fdt(bytearray(blob)); four.resize(len(blob)+2048) + reg = module.cells(four, "/nvme@60400000", "reg") + module.put_cells(four, "/nvme@60400000", "reg", reg[:8]+[0,0x60300000,0,0x30000]+reg[8:]) + module.put_strings(four, "/nvme@60400000", "reg-names", ["nvme","ans","nvmmu","sptm"]) + four.pack() + converted = libfdt.Fdt(bytearray(module.migrate(bytes(four.as_bytearray())))) + assert module.cells(converted, "/nvme@60400000", "reg") == reg[:8] + pci = libfdt.Fdt(bytearray(blob)); pci.resize(len(blob)+4096) + pci.add_subnode(0, "pcie@62000000") + path = "/pcie@62000000" + module.put_strings(pci,path,"compatible",["apple,x1n1-pcie"]) + module.put(pci,path,"apple,preinitialized",b"") + module.put_cells(pci,path,"max-link-speed",[3]); module.put_cells(pci,path,"num-lanes",[1]) + module.put_cells(pci,path,"apple,rid-to-sid",[1,0x100,1]) + original=[] + for address,size in [(0x62000000,0x200000),(0x62800000,0x8000),(0x62808000,0x4000),(0x6280c000,0x4000),(0x62810140,4)]: + original += [0,address,0,size] + module.put_cells(pci,path,"reg",original) + module.put_strings(pci,path,"reg-names",["config","port","phy","intr2axi","perst"]) + pci.pack() + converted=libfdt.Fdt(bytearray(module.migrate(bytes(pci.as_bytearray()),"t6050-gen3-x1"))) + assert module.strings(converted,path,"reg-names")==["ecam","port","phy","perst","intr2axi"] + assert module.cells(converted,path,"reg")==original[:12]+original[16:20]+original[12:16] + for mutation in ["live", "duplicate", "collision", "unpowered-usb-dart"]: + bad = libfdt.Fdt(bytearray(blob)); bad.resize(len(blob)+2048) + if mutation == "live": + module.delete(bad, "/nvme@60400000", "asahi,rtkit-quiesced") + elif mutation == "duplicate": + handles = module.cells(bad, "/nvme@60400000", "memory-region") + handles[1] = handles[0] + module.put_cells(bad, "/nvme@60400000", "memory-region", handles) + elif mutation == "unpowered-usb-dart": + module.delete(bad, "/iommu@61f38000", "power-domains") + else: + bad.add_subnode(0, "device@61ff0000") + module.put_cells(bad, "/device@61ff0000", "reg", [0, module.APIF_IPA, 0, 4]) + bad.pack() + try: + module.migrate(bytes(bad.as_bytearray())) + raise AssertionError(f"accepted {mutation} handoff") + except ValueError: + pass + print("ok 1 - checked guest migration and preserved producer metadata") + return 0 + + +if __name__ == "__main__": + sys.exit(main())