| #!/usr/bin/env python3 | |
| # | |
| # Boots a nested guest and compare content of a device (passthrough) to a | |
| # reference image. Both vfio group and iommufd passthrough methods are tested. | |
| # | |
| # Copyright (c) 2025 Linaro Ltd. | |
| # | |
| # Author: Pierrick Bouvier <pierrick.bouvier@linaro.org> | |
| # | |
| # SPDX-License-Identifier: GPL-2.0-or-later | |
| from os.path import join | |
| from random import randbytes | |
| from qemu_test import QemuSystemTest, Asset | |
| from qemu_test import wait_for_console_pattern | |
| GUEST_SCRIPT = ''' | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| set -x | |
| # find disks from nvme serial | |
| dev_vfio=$(lsblk --nvme | grep vfio | cut -f 1 -d ' ') | |
| dev_iommufd=$(lsblk --nvme | grep iommufd | cut -f 1 -d ' ') | |
| pci_vfio=$(basename $(readlink -f /sys/block/$dev_vfio/../../../)) | |
| pci_iommufd=$(basename $(readlink -f /sys/block/$dev_iommufd/../../../)) | |
| # bind disks to vfio | |
| for p in "$pci_vfio" "$pci_iommufd"; do | |
| if [ "$(cat /sys/bus/pci/devices/$p/driver_override)" == vfio-pci ]; then | |
| continue | |
| fi | |
| echo $p > /sys/bus/pci/drivers/nvme/unbind | |
| echo vfio-pci > /sys/bus/pci/devices/$p/driver_override | |
| echo $p > /sys/bus/pci/drivers/vfio-pci/bind | |
| done | |
| # boot nested guest and execute /host/nested_guest.sh | |
| # one disk is passed through vfio group, the other, through iommufd | |
| qemu-system-aarch64 \ | |
| -M virt \ | |
| -display none \ | |
| -serial stdio \ | |
| -cpu host \ | |
| -enable-kvm \ | |
| -m 1G \ | |
| -kernel /host/Image.gz \ | |
| -drive format=raw,file=/host/guest.ext4,if=virtio \ | |
| -append "root=/dev/vda init=/init -- bash /host/nested_guest.sh" \ | |
| -virtfs local,path=/host,mount_tag=host,security_model=mapped,readonly=off \ | |
| -device vfio-pci,host=$pci_vfio \ | |
| -object iommufd,id=iommufd0 \ | |
| -device vfio-pci,host=$pci_iommufd,iommufd=iommufd0 | |
| ''' | |
| NESTED_GUEST_SCRIPT = ''' | |
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| set -x | |
| image_vfio=/host/disk_vfio | |
| image_iommufd=/host/disk_iommufd | |
| dev_vfio=$(lsblk --nvme | grep vfio | cut -f 1 -d ' ') | |
| dev_iommufd=$(lsblk --nvme | grep iommufd | cut -f 1 -d ' ') | |
| # compare if devices are identical to original images | |
| diff $image_vfio /dev/$dev_vfio | |
| diff $image_iommufd /dev/$dev_iommufd | |
| echo device_passthrough_test_ok | |
| ''' | |
| class Aarch64DevicePassthrough(QemuSystemTest): | |
| # https://github.com/p-b-o/qemu-linux-stack/tree/device_passthrough | |
| # $ ./build.sh && ./archive_artifacts.sh out.tar.xz | |
| # | |
| # Linux kernel is compiled with defconfig + | |
| # IOMMUFD + VFIO_DEVICE_CDEV + ARM_SMMU_V3_IOMMUFD | |
| # https://docs.kernel.org/driver-api/vfio.html#vfio-device-cde | |
| ASSET_DEVICE_PASSTHROUGH_STACK = Asset( | |
| ('https://github.com/p-b-o/qemu-linux-stack/' | |
| 'releases/download/build/device_passthrough-a9612a2.tar.xz'), | |
| 'f7d2f70912e7231986e6e293e1a2c4786dd02bec113a7acb6bfc619e96155455') | |
| # This tests the device passthrough implementation, by booting a VM | |
| # supporting it with two nvme disks attached, and launching a nested VM | |
| # reading their content. | |
| def test_aarch64_device_passthrough(self): | |
| self.set_machine('virt') | |
| self.require_accelerator('tcg') | |
| self.vm.set_console() | |
| stack_path_tar = self.ASSET_DEVICE_PASSTHROUGH_STACK.fetch() | |
| self.archive_extract(stack_path_tar, format="tar") | |
| stack = self.scratch_file('out') | |
| kernel = join(stack, 'Image.gz') | |
| rootfs_host = join(stack, 'host.ext4') | |
| disk_vfio = join(stack, 'disk_vfio') | |
| disk_iommufd = join(stack, 'disk_iommufd') | |
| guest_cmd = join(stack, 'guest.sh') | |
| nested_guest_cmd = join(stack, 'nested_guest.sh') | |
| # we generate two random disks | |
| with open(disk_vfio, "wb") as d: | |
| d.write(randbytes(512)) | |
| with open(disk_iommufd, "wb") as d: | |
| d.write(randbytes(1024)) | |
| with open(guest_cmd, 'w', encoding='utf-8') as s: | |
| s.write(GUEST_SCRIPT) | |
| with open(nested_guest_cmd, 'w', encoding='utf-8') as s: | |
| s.write(NESTED_GUEST_SCRIPT) | |
| self.vm.add_args('-cpu', 'max') | |
| self.vm.add_args('-m', '2G') | |
| self.vm.add_args('-M', 'virt,' | |
| 'virtualization=on,' | |
| 'gic-version=max,' | |
| 'iommu=smmuv3') | |
| self.vm.add_args('-kernel', kernel) | |
| self.vm.add_args('-drive', f'format=raw,file={rootfs_host}') | |
| self.vm.add_args('-drive', | |
| f'file={disk_vfio},if=none,id=vfio,format=raw') | |
| self.vm.add_args('-device', 'nvme,serial=vfio,drive=vfio') | |
| self.vm.add_args('-drive', | |
| f'file={disk_iommufd},if=none,id=iommufd,format=raw') | |
| self.vm.add_args('-device', 'nvme,serial=iommufd,drive=iommufd') | |
| self.vm.add_args('-virtfs', | |
| f'local,path={stack}/,mount_tag=host,' | |
| 'security_model=mapped,readonly=off') | |
| # boot and execute guest script | |
| # init will trigger a kernel panic if script fails | |
| self.vm.add_args('-append', | |
| 'root=/dev/vda init=/init -- bash /host/guest.sh') | |
| self.vm.launch() | |
| wait_for_console_pattern(self, 'device_passthrough_test_ok', | |
| failure_message='Kernel panic') | |
| if __name__ == '__main__': | |
| QemuSystemTest.main() |