From 4a271204ead1fe1cd71c5d0cc63bd6f5331946ae Mon Sep 17 00:00:00 2001 From: Dj Date: Sat, 10 Oct 2026 23:36:29 -0400 Subject: [PATCH] arm64: apple: Add native APIF guest config and checked DT migration Enable the native transport and its non-GPU consumers in an explicit config fragment, including the built-in SART dependency. Preserve board addresses and ownership in producer-generated descriptions rather than inventing a new static M5 board tree. Migrate version-1 DART/SART providers while retaining apertures, firmware streams and stable phandles. Repair standard provider aliases, declare the PMP aperture guard explicitly, and use the separate native MMIO page. Refuse incomplete ANS queue ownership and undescribed PCIe layouts before writing a new output file. Leave unrelated device data unchanged. Document boot-producer responsibilities and qualification limits. Host DT tests cover preserved metadata, duplicate queues, live handoffs and CPU resource collisions. The provider config merge passes olddefconfig; this utility does not install or qualify hardware. Signed-off-by: Dj --- MAINTAINERS | 9 +- arch/arm64/configs/aurora-apif.config | 14 + tools/aurora-apif/README.rst | 51 ++++ tools/aurora-apif/prepare_guest.py | 274 +++++++++++++++++++ tools/testing/selftests/aurora-apif/Makefile | 2 +- tools/testing/selftests/aurora-apif/guest.py | 96 +++++++ 6 files changed, 444 insertions(+), 2 deletions(-) create mode 100644 arch/arm64/configs/aurora-apif.config create mode 100644 tools/aurora-apif/README.rst create mode 100755 tools/aurora-apif/prepare_guest.py create mode 100755 tools/testing/selftests/aurora-apif/guest.py 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())