diff --git a/MAINTAINERS b/MAINTAINERS index 0e00f210893f1..58cd37d740fd4 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -4365,7 +4365,14 @@ S: Maintained F: Documentation/devicetree/bindings/firmware/aurora,apif-mmio.yaml F: Documentation/devicetree/bindings/iommu/apple,dart-apif.yaml F: Documentation/devicetree/bindings/soc/apple/apple,sart-apif.yaml -F: Documentation/driver-api/firmware/aurora-apif.rst +F: Documentation/driver-api/firmware/aurora-apif*.rst +F: Documentation/devicetree/bindings/nvme/apple,nvme-apif.yaml +F: Documentation/devicetree/bindings/pci/apple,apif-pcie.yaml +F: arch/arm64/configs/aurora-apif.config +F: drivers/nvme/host/apple-apif.h +F: drivers/pci/controller/apple-apif/ +F: include/linux/pci-apple-apif.h +F: tools/aurora-apif/ F: drivers/firmware/aurora-apif-mmio.c F: drivers/iommu/apple-dart-apif.c F: drivers/soc/apple/apple-sart-apif.c diff --git a/arch/arm64/configs/aurora-apif.config b/arch/arm64/configs/aurora-apif.config new file mode 100644 index 0000000000000..84234623ca4ce --- /dev/null +++ b/arch/arm64/configs/aurora-apif.config @@ -0,0 +1,14 @@ +# Native Aura guest providers. Apply after defconfig and asahi.config. +CONFIG_ARM64_16K_PAGES=y +CONFIG_AURORA_APIF_MMIO=y +CONFIG_APPLE_DART_APIF=y +# The alternate SART provider is built into the common SART consumer API. +CONFIG_APPLE_SART=y +CONFIG_APPLE_SART_APIF=y +CONFIG_NVME_APPLE=y +CONFIG_PCI=y +CONFIG_PCI_MSI=y +CONFIG_PCIE_APPLE_APIF=y +CONFIG_PM=y +CONFIG_RUST=y +CONFIG_APPLE_PMP=y diff --git a/tools/aurora-apif/README.rst b/tools/aurora-apif/README.rst new file mode 100644 index 0000000000000..75e2468d29a75 --- /dev/null +++ b/tools/aurora-apif/README.rst @@ -0,0 +1,51 @@ +.. 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. diff --git a/tools/aurora-apif/prepare_guest.py b/tools/aurora-apif/prepare_guest.py new file mode 100755 index 0000000000000..65f30177beabf --- /dev/null +++ b/tools/aurora-apif/prepare_guest.py @@ -0,0 +1,274 @@ +#!/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 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") + 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} + 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") + 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 != ["nvme", "ans", "sptm"] or len(reg) != 3 * 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]) + # 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") + 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) + 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/Makefile b/tools/testing/selftests/aurora-apif/Makefile index e77c69c3de783..5c75739110910 100644 --- a/tools/testing/selftests/aurora-apif/Makefile +++ b/tools/testing/selftests/aurora-apif/Makefile @@ -1,3 +1,3 @@ # SPDX-License-Identifier: GPL-2.0-only -TEST_PROGS := apif_mmio.py pmp.py +TEST_PROGS := apif_mmio.py pmp.py guest.py include ../lib.mk diff --git a/tools/testing/selftests/aurora-apif/guest.py b/tools/testing/selftests/aurora-apif/guest.py new file mode 100755 index 0000000000000..5783f64c1a305 --- /dev/null +++ b/tools/testing/selftests/aurora-apif/guest.py @@ -0,0 +1,96 @@ +#!/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";}; + gpu {compatible="example,opaque-gpu"; unchanged=[de ad be ef];}; +};''' + 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] + for mutation in ["live", "duplicate", "collision"]: + 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) + 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())