[RFC PATCH 08/10] vfio/apple: Add IOMMU container and PCI device

Scott J. Goldman posted 10 patches 6 days, 11 hours ago
Maintainers: Paolo Bonzini <pbonzini@redhat.com>, Roman Bolshakov <rbolshakov@ddn.com>, Phil Dennis-Jordan <phil@philjordan.eu>, "Michael S. Tsirkin" <mst@redhat.com>, Marcel Apfelbaum <marcel.apfelbaum@gmail.com>, Pierrick Bouvier <pierrick.bouvier@linaro.org>, John Levon <john.levon@nutanix.com>, Thanos Makatos <thanos.makatos@nutanix.com>, "Cédric Le Goater" <clg@redhat.com>, Alex Williamson <alex@shazbot.org>, Tony Krowiak <akrowiak@linux.ibm.com>, Halil Pasic <pasic@linux.ibm.com>, Jason Herne <jjherne@linux.ibm.com>, Cornelia Huck <cohuck@redhat.com>, Eric Farman <farman@linux.ibm.com>, Matthew Rosato <mjrosato@linux.ibm.com>, "Scott J. Goldman" <scottjgo@gmail.com>, "Marc-André Lureau" <marcandre.lureau@redhat.com>, "Daniel P. Berrangé" <berrange@redhat.com>, "Philippe Mathieu-Daudé" <philmd@linaro.org>
[RFC PATCH 08/10] vfio/apple: Add IOMMU container and PCI device
Posted by Scott J. Goldman 6 days, 11 hours ago
From: "Scott J. Goldman" <scottjg@umich.edu>

Add the core Apple VFIO backend: an IOMMU container type and a PCI
device type that together provide VFIO passthrough on macOS via the
VFIOUserPCIDriver DriverKit extension.

AppleVFIOContainer (container-apple.c):
  Implements VFIOIOMMUClass for DMA map/unmap by calling through to the
  dext client's register/unregister_dma functions.  Synthesizes
  VFIO_DEVICE_GET_INFO responses with appropriate flags for the
  passthrough device.

VFIOApplePCIDevice (apple-device.c):
  QOM type "vfio-apple-pci" subclassing VFIOPCIDevice.  Provides the
  full VFIODeviceIOOps implementation:
  - Config space reads forwarded to the dext; writes filtered to block
    BAR and status register reprogramming (macOS/DART owns those).
  - PCI COMMAND register writes forwarded for bus-master/memory-space
    enable.
  - BAR regions directly mapped via IOConnectMapMemory64 and accessed
    as host MMIO loads/stores.
  - MSI/MSI-X interrupt delivery through a bitmap-poll model with
    async GCD notification bridged to an EventNotifier.
  - Device reset via the dext (IOPCIDevice::Reset FLR/hot-reset).
  - Shared dext connection management for multi-function devices.

apple.h defines the shared types: AppleVFIOContainer, AppleVFIOState,
AppleVFIOBarMap, and VFIOApplePCIDevice.

Signed-off-by: Scott J. Goldman <scottjgo@gmail.com>
---
 hw/vfio/apple-device.c    | 945 ++++++++++++++++++++++++++++++++++++++
 hw/vfio/apple.h           |  74 +++
 hw/vfio/container-apple.c | 241 ++++++++++
 hw/vfio/meson.build       |   6 +-
 4 files changed, 1264 insertions(+), 2 deletions(-)
 create mode 100644 hw/vfio/apple-device.c
 create mode 100644 hw/vfio/apple.h
 create mode 100644 hw/vfio/container-apple.c

diff --git a/hw/vfio/apple-device.c b/hw/vfio/apple-device.c
new file mode 100644
index 0000000000..9291ac845b
--- /dev/null
+++ b/hw/vfio/apple-device.c
@@ -0,0 +1,945 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Apple/macOS VFIO PCI device passthrough via DriverKit dext.
+ *
+ * Copyright (c) 2026 Scott J. Goldman
+ */
+
+#include "qemu/osdep.h"
+
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <linux/vfio.h>
+
+#include "apple-dext-client.h"
+#include "hw/vfio/apple.h"
+#include "hw/vfio/vfio-container.h"
+#include "qapi/error.h"
+#include "qemu/error-report.h"
+#include "qemu/host-pci-mmio.h"
+#include "qemu/main-loop.h"
+
+typedef struct AppleVFIOSharedDext {
+    io_connect_t conn;
+    uint32_t refs;
+} AppleVFIOSharedDext;
+
+typedef struct AppleVFIODMAProbe {
+    uint64_t managed_bdf;
+    uint64_t host_bus;
+    uint64_t host_device;
+    uint64_t host_function;
+    DeviceState *match;
+} AppleVFIODMAProbe;
+
+static GHashTable *apple_vfio_shared_dexts;
+
+static inline guint apple_vfio_dext_key(uint8_t bus, uint8_t device,
+                                        uint8_t function)
+{
+    return ((guint)bus << 16) | ((guint)device << 8) | function;
+}
+
+static inline AppleVFIOContainer *apple_vfio_container(VFIODevice *vbasedev)
+{
+    return VFIO_IOMMU_APPLE(vbasedev->bcontainer);
+}
+
+static inline io_connect_t apple_vfio_connection(VFIODevice *vbasedev)
+{
+    AppleVFIOContainer *container = apple_vfio_container(vbasedev);
+
+    return container ? container->dext_conn : IO_OBJECT_NULL;
+}
+
+static void apple_vfio_find_dma_companion_cb(PCIBus *bus, PCIDevice *pdev,
+                                             void *opaque)
+{
+    AppleVFIODMAProbe *probe = opaque;
+    Error *err = NULL;
+    uint64_t managed_bdf;
+    uint64_t host_bus;
+    uint64_t host_device;
+    uint64_t host_function;
+
+    if (probe->match ||
+        !object_dynamic_cast(OBJECT(pdev), "apple-dma-pci")) {
+        return;
+    }
+
+    managed_bdf = object_property_get_uint(OBJECT(pdev), "managed-bdf", &err);
+    if (err) {
+        error_free(err);
+        return;
+    }
+
+    host_bus = object_property_get_uint(OBJECT(pdev), "x-apple-host-bus", &err);
+    if (err) {
+        error_free(err);
+        return;
+    }
+
+    host_device = object_property_get_uint(OBJECT(pdev),
+                                           "x-apple-host-device", &err);
+    if (err) {
+        error_free(err);
+        return;
+    }
+
+    host_function = object_property_get_uint(OBJECT(pdev),
+                                             "x-apple-host-function", &err);
+    if (err) {
+        error_free(err);
+        return;
+    }
+
+    if (managed_bdf == probe->managed_bdf &&
+        host_bus == probe->host_bus &&
+        host_device == probe->host_device &&
+        host_function == probe->host_function) {
+        probe->match = DEVICE(pdev);
+    }
+}
+
+static DeviceState *apple_vfio_find_dma_companion(VFIOApplePCIDevice *adev)
+{
+    VFIOPCIDevice *vdev = VFIO_PCI_DEVICE(adev);
+    PCIDevice *pdev = PCI_DEVICE(vdev);
+    AppleVFIODMAProbe probe = {
+        .managed_bdf = PCI_BUILD_BDF(pci_dev_bus_num(pdev), pdev->devfn),
+        .host_bus = vdev->host.bus,
+        .host_device = vdev->host.slot,
+        .host_function = vdev->host.function,
+    };
+
+    pci_for_each_device_under_bus(pci_device_root_bus(pdev),
+                                  apple_vfio_find_dma_companion_cb, &probe);
+    return probe.match;
+}
+
+static void apple_vfio_signal_irqfd(int fd)
+{
+    static const uint64_t value = 1;
+    ssize_t ret;
+
+    if (fd < 0) {
+        return;
+    }
+
+    do {
+        ret = write(fd, &value, sizeof(value));
+    } while (ret < 0 && errno == EINTR);
+}
+
+static void apple_vfio_deliver_irq(VFIOPCIDevice *vdev, uint32_t vector)
+{
+    switch (vdev->interrupt) {
+    case VFIO_INT_MSI:
+    case VFIO_INT_MSIX:
+        if (vector < vdev->nr_vectors && vdev->msi_vectors[vector].use) {
+            apple_vfio_signal_irqfd(
+                event_notifier_get_wfd(&vdev->msi_vectors[vector].interrupt));
+        }
+        break;
+    case VFIO_INT_INTx:
+        apple_vfio_signal_irqfd(
+            event_notifier_get_wfd(&vdev->intx.interrupt));
+        break;
+    default:
+        break;
+    }
+}
+
+/*
+ * Called on a GCD dispatch queue when the dext signals pending interrupts.
+ * Just pokes the EventNotifier to wake the QEMU main loop.
+ */
+static void apple_vfio_irq_wakeup(void *opaque)
+{
+    VFIOApplePCIDevice *adev = opaque;
+
+    event_notifier_set(&adev->apple->irq_notifier);
+}
+
+/*
+ * QEMU main-loop fd handler: drain the pending-interrupt bitfield from
+ * the dext, deliver each flagged vector, then re-arm the async wait.
+ */
+static void apple_vfio_irq_handler(void *opaque)
+{
+    VFIOApplePCIDevice *adev = opaque;
+    VFIOPCIDevice *vdev = VFIO_PCI_DEVICE(adev);
+    VFIODevice *vbasedev = &vdev->vbasedev;
+    io_connect_t conn = apple_vfio_connection(vbasedev);
+    AppleVFIOState *apple = adev->apple;
+    uint64_t pending[4];
+    int word;
+
+    if (!event_notifier_test_and_clear(&apple->irq_notifier)) {
+        return;
+    }
+
+    if (conn == IO_OBJECT_NULL) {
+        return;
+    }
+
+    if (apple_dext_read_pending_irqs(conn, pending) != KERN_SUCCESS) {
+        apple_dext_interrupt_notify_rearm(apple->irq_notify);
+        return;
+    }
+
+    for (word = 0; word < 4; word++) {
+        uint64_t bits = pending[word];
+
+        while (bits) {
+            int bit = __builtin_ctzll(bits);
+            uint32_t vector = word * 64 + bit;
+
+            apple_vfio_deliver_irq(vdev, vector);
+            bits &= bits - 1;
+        }
+    }
+
+    apple_dext_interrupt_notify_rearm(apple->irq_notify);
+}
+
+bool apple_vfio_get_bar_info(VFIOApplePCIDevice *adev, uint8_t bar,
+                             uint8_t *mem_idx, uint64_t *size,
+                             uint8_t *type)
+{
+    io_connect_t conn = apple_vfio_connection(&VFIO_PCI_DEVICE(adev)->vbasedev);
+
+    if (conn != IO_OBJECT_NULL) {
+        return apple_dext_get_bar_info(conn, bar, mem_idx, size, type) ==
+               KERN_SUCCESS;
+    }
+
+    if (mem_idx) {
+        *mem_idx = 0;
+    }
+    if (size) {
+        *size = 0;
+    }
+    if (type) {
+        *type = 0;
+    }
+    return false;
+}
+
+static void apple_vfio_pci_init(VFIOApplePCIDevice *adev)
+{
+    VFIOPCIDevice *vdev = VFIO_PCI_DEVICE(adev);
+
+    /*
+     * On macOS, HVF can only map on 16kb page boundaries, so these quirk
+     * fixes end up breaking things. Likewise the performance enhancements
+     * there rely on kvm-specific features. Disable for now, but we should
+     * revisit this.
+     */
+    vdev->no_bar_quirks = true;
+}
+
+static bool apple_vfio_pci_pre_realize(VFIOApplePCIDevice *adev, Error **errp)
+{
+    VFIOPCIDevice *vdev = VFIO_PCI_DEVICE(adev);
+    VFIODevice *vbasedev = &vdev->vbasedev;
+
+    adev->apple = g_new0(AppleVFIOState, 1);
+
+    if (!vbasedev->name) {
+        vbasedev->name = g_strdup_printf("apple-%04x:%02x:%02x.%x",
+                                         vdev->host.domain,
+                                         vdev->host.bus,
+                                         vdev->host.slot,
+                                         vdev->host.function);
+    }
+
+    return true;
+}
+
+static bool apple_vfio_create_dma_companion(VFIOApplePCIDevice *adev,
+                                            Error **errp)
+{
+    VFIOPCIDevice *vdev = VFIO_PCI_DEVICE(adev);
+    PCIDevice *pdev = PCI_DEVICE(vdev);
+    DeviceState *dev;
+
+    if (adev->dma_companion_autocreated && adev->dma_companion) {
+        return true;
+    }
+
+    if (apple_vfio_find_dma_companion(adev) != NULL) {
+        return true;
+    }
+
+    dev = qdev_new("apple-dma-pci");
+    if (!object_property_set_uint(OBJECT(dev), "managed-bdf",
+                                  PCI_BUILD_BDF(pci_dev_bus_num(pdev),
+                                                pdev->devfn), errp) ||
+        !object_property_set_uint(OBJECT(dev), "x-apple-host-bus",
+                                  vdev->host.bus, errp) ||
+        !object_property_set_uint(OBJECT(dev), "x-apple-host-device",
+                                  vdev->host.slot, errp) ||
+        !object_property_set_uint(OBJECT(dev), "x-apple-host-function",
+                                  vdev->host.function, errp)) {
+        object_unref(OBJECT(dev));
+        return false;
+    }
+
+    if (!qdev_realize(dev, BUS(pci_get_bus(pdev)), errp)) {
+        object_unref(OBJECT(dev));
+        return false;
+    }
+
+    adev->dma_companion = dev;
+    adev->dma_companion_autocreated = true;
+    object_unref(OBJECT(dev));
+    return true;
+}
+
+static void apple_vfio_destroy_dma_companion(VFIOApplePCIDevice *adev)
+{
+    if (!adev->dma_companion_autocreated || adev->dma_companion == NULL) {
+        return;
+    }
+
+    object_unparent(OBJECT(adev->dma_companion));
+    adev->dma_companion = NULL;
+    adev->dma_companion_autocreated = false;
+}
+
+bool apple_vfio_device_setup(VFIOApplePCIDevice *adev, Error **errp)
+{
+    VFIODevice *vbasedev = &VFIO_PCI_DEVICE(adev)->vbasedev;
+    io_connect_t conn = apple_vfio_connection(vbasedev);
+    uint32_t num_vectors = 0;
+    kern_return_t kr;
+
+    if (conn == IO_OBJECT_NULL) {
+        error_setg(errp, "vfio-apple: missing dext connection");
+        return false;
+    }
+
+    kr = apple_dext_setup_interrupts(conn, &num_vectors);
+    if (kr != KERN_SUCCESS) {
+        error_setg(errp, "vfio-apple: failed to setup interrupts (kr=0x%x)",
+                   kr);
+        return false;
+    }
+
+    adev->apple->num_irq_vectors = num_vectors;
+
+    if (event_notifier_init(&adev->apple->irq_notifier, false) < 0) {
+        error_setg(errp, "vfio-apple: failed to create IRQ event notifier");
+        return false;
+    }
+
+    qemu_set_fd_handler(event_notifier_get_fd(&adev->apple->irq_notifier),
+                        apple_vfio_irq_handler, NULL, adev);
+
+    adev->apple->irq_notify =
+        apple_dext_interrupt_notify_create(conn, apple_vfio_irq_wakeup, adev);
+    if (!adev->apple->irq_notify) {
+        error_setg(errp,
+                   "vfio-apple: failed to create IRQ async notification");
+        qemu_set_fd_handler(
+            event_notifier_get_fd(&adev->apple->irq_notifier),
+            NULL, NULL, NULL);
+        event_notifier_cleanup(&adev->apple->irq_notifier);
+        return false;
+    }
+
+    return true;
+}
+
+void apple_vfio_device_cleanup(VFIOApplePCIDevice *adev)
+{
+    AppleVFIOState *apple = adev->apple;
+
+    if (!apple) {
+        return;
+    }
+
+    if (apple->irq_notify) {
+        apple_dext_interrupt_notify_destroy(apple->irq_notify);
+        apple->irq_notify = NULL;
+
+        qemu_set_fd_handler(event_notifier_get_fd(&apple->irq_notifier),
+                            NULL, NULL, NULL);
+        event_notifier_cleanup(&apple->irq_notifier);
+    }
+}
+
+static int apple_vfio_device_feature(VFIODevice *vdev,
+                                     struct vfio_device_feature *feat)
+{
+    return -ENOTTY;
+}
+
+static int apple_vfio_device_reset(VFIODevice *vbasedev)
+{
+    io_connect_t conn = apple_vfio_connection(vbasedev);
+
+    if (conn == IO_OBJECT_NULL) {
+        return -ENODEV;
+    }
+
+    return apple_dext_reset_device(conn) == KERN_SUCCESS ? 0 : -EIO;
+}
+
+static int apple_vfio_get_region_info(VFIODevice *vbasedev,
+                                      struct vfio_region_info *info,
+                                      int *fd)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(vbasedev->dev);
+    uint32_t index = info->index;
+    uint64_t size = 0;
+
+    if (fd) {
+        *fd = -1;
+    }
+
+    memset((char *)info + offsetof(struct vfio_region_info, flags), 0,
+           sizeof(*info) - offsetof(struct vfio_region_info, flags));
+
+    info->index = index;
+    info->flags = VFIO_REGION_INFO_FLAG_READ | VFIO_REGION_INFO_FLAG_WRITE;
+    info->offset = (uint64_t)index << 20;
+
+    switch (info->index) {
+    case VFIO_PCI_BAR0_REGION_INDEX ... VFIO_PCI_BAR5_REGION_INDEX:
+        if (!apple_vfio_get_bar_info(adev, info->index, NULL, &size, NULL)) {
+            size = 0;
+        }
+        info->size = size;
+        info->flags |= VFIO_REGION_INFO_FLAG_MMAP;
+        break;
+    case VFIO_PCI_CONFIG_REGION_INDEX:
+        info->size = PCIE_CONFIG_SPACE_SIZE;
+        break;
+    case VFIO_PCI_ROM_REGION_INDEX:
+    case VFIO_PCI_VGA_REGION_INDEX:
+        info->size = 0;
+        break;
+    default:
+        return -EINVAL;
+    }
+
+    return 0;
+}
+
+static int apple_vfio_get_irq_info(VFIODevice *vbasedev,
+                                   struct vfio_irq_info *info)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(vbasedev->dev);
+    VFIOPCIDevice *vdev = VFIO_PCI_DEVICE(adev);
+
+    switch (info->index) {
+    case VFIO_PCI_MSI_IRQ_INDEX:
+        info->flags = VFIO_IRQ_INFO_EVENTFD;
+        info->count = adev->apple->num_irq_vectors;
+        break;
+    case VFIO_PCI_MSIX_IRQ_INDEX:
+        info->flags = VFIO_IRQ_INFO_EVENTFD | VFIO_IRQ_INFO_NORESIZE;
+        info->count = vdev->msix ? vdev->msix->entries : 0;
+        break;
+    case VFIO_PCI_INTX_IRQ_INDEX:
+        info->flags = VFIO_IRQ_INFO_EVENTFD;
+        info->count = 1;
+        break;
+    case VFIO_PCI_ERR_IRQ_INDEX:
+    case VFIO_PCI_REQ_IRQ_INDEX:
+        /*
+         * Apple dext passthrough has no kernel-side AER or device-request
+         * notification currently; return count 0 to tell the core to skip
+         * these.
+         */
+        info->flags = 0;
+        info->count = 0;
+        break;
+    default:
+        return -EINVAL;
+    }
+
+    return 0;
+}
+
+static void apple_vfio_update_irq_mask(VFIODevice *vbasedev)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(vbasedev->dev);
+    VFIOPCIDevice *vdev = VFIO_PCI_DEVICE(adev);
+    io_connect_t conn = apple_vfio_connection(vbasedev);
+    uint64_t mask[4] = {0};
+    uint32_t i;
+
+    if (conn == IO_OBJECT_NULL) {
+        return;
+    }
+
+    switch (vdev->interrupt) {
+    case VFIO_INT_MSI:
+    case VFIO_INT_MSIX:
+        for (i = 0; i < vdev->nr_vectors; i++) {
+            if (vdev->msi_vectors[i].use) {
+                mask[i / 64] |= 1ULL << (i % 64);
+            }
+        }
+        break;
+    case VFIO_INT_INTx:
+        mask[0] = 1;
+        break;
+    default:
+        break;
+    }
+
+    apple_dext_set_irq_mask(conn, mask);
+}
+
+static int apple_vfio_set_irqs(VFIODevice *vbasedev, struct vfio_irq_set *irq)
+{
+    apple_vfio_update_irq_mask(vbasedev);
+    return 0;
+}
+
+static int apple_vfio_bar_read(VFIODevice *vbasedev, uint8_t nr, off_t off,
+                               uint32_t size, void *data)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(vbasedev->dev);
+    AppleVFIOBarMap *bm = &adev->apple->bar_maps[nr];
+    const void *p;
+    uint64_t value;
+
+    if (!bm->addr || off + size > bm->size) {
+        error_report("vfio-apple: BAR%d read out of range or unmapped", nr);
+        return -EINVAL;
+    }
+
+    if (size != 1 && size != 2 && size != 4 && size != 8) {
+        return -EINVAL;
+    }
+
+    p = (const char *)bm->addr + off;
+    value = host_pci_ldn_le_p(p, size);
+    memcpy(data, &value, size);
+
+    return size;
+}
+
+static int apple_vfio_region_read(VFIODevice *vbasedev, uint8_t nr, off_t off,
+                                  uint32_t size, void *data)
+{
+    io_connect_t conn = apple_vfio_connection(vbasedev);
+    kern_return_t kr;
+    uint32_t legacy_size = 0;
+
+    if (nr != VFIO_PCI_CONFIG_REGION_INDEX) {
+        return apple_vfio_bar_read(vbasedev, nr, off, size, data);
+    }
+
+    if (conn == IO_OBJECT_NULL) {
+        return -ENODEV;
+    }
+
+    legacy_size = MIN(size, PCIE_CONFIG_SPACE_SIZE - off);
+
+    if (legacy_size == 1 || legacy_size == 2 || legacy_size == 4) {
+        uint64_t value = 0;
+
+        kr = apple_dext_config_read(conn, off, legacy_size, &value);
+        if (kr != KERN_SUCCESS) {
+            return -EIO;
+        }
+
+        memcpy(data, &value, legacy_size);
+        if (legacy_size < size) {
+            memset((uint8_t *)data + legacy_size, 0, size - legacy_size);
+        }
+        return size;
+    }
+
+    kr = apple_dext_config_read_block(conn, off, data, legacy_size);
+    if (kr != KERN_SUCCESS) {
+        return -EIO;
+    }
+    if (legacy_size < size) {
+        memset((uint8_t *)data + legacy_size, 0, size - legacy_size);
+    }
+    return size;
+}
+
+static bool apple_vfio_config_write_is_safe(off_t off, uint32_t size)
+{
+    off_t end = off + size;
+
+    /*
+     * Block writes that would reprogram the device's bus identity or
+     * address decoders.  macOS / DART owns those registers; touching
+     * them from the guest breaks the IOKit mapping and the device
+     * "falls off the bus."
+     *
+     * Everything else (vendor capabilities, MSI/MSI-X, PCIe cap, etc.)
+     * is forwarded.
+     */
+
+    /* PCI_STATUS stays emulated/blocked */
+    if (off < PCI_STATUS + 2 && end > PCI_STATUS) {
+        return false;
+    }
+
+    /* BAR0-BAR5 */
+    if (off < PCI_BASE_ADDRESS_5 + 4 && end > PCI_BASE_ADDRESS_0) {
+        return false;
+    }
+
+    return true;
+}
+
+static int apple_vfio_forward_command_write(io_connect_t conn, off_t off,
+                                            uint32_t size, const void *data)
+{
+    const uint8_t *bytes = data;
+    off_t end = off + size;
+    off_t cmd_start = MAX(off, (off_t)PCI_COMMAND);
+    off_t cmd_end = MIN(end, (off_t)(PCI_COMMAND + 2));
+    off_t pos;
+
+    if (conn == IO_OBJECT_NULL) {
+        return -ENODEV;
+    }
+
+    for (pos = cmd_start; pos < cmd_end; pos++) {
+        uint64_t value = bytes[pos - off];
+        kern_return_t kr = apple_dext_config_write(conn, pos, 1, value);
+
+        if (kr != KERN_SUCCESS) {
+            return -EIO;
+        }
+    }
+
+    return 0;
+}
+
+static int apple_vfio_bar_write(VFIODevice *vbasedev, uint8_t nr, off_t off,
+                                uint32_t size, void *data)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(vbasedev->dev);
+    AppleVFIOBarMap *bm = &adev->apple->bar_maps[nr];
+    void *p;
+    uint64_t value = 0;
+
+    if (!bm->addr || off + size > bm->size) {
+        error_report("vfio-apple: BAR%d write out of range or unmapped", nr);
+        return -EINVAL;
+    }
+
+    if (size != 1 && size != 2 && size != 4 && size != 8) {
+        return -EINVAL;
+    }
+
+    p = (char *)bm->addr + off;
+    memcpy(&value, data, size);
+    host_pci_stn_le_p(p, size, value);
+
+    return size;
+}
+
+static int apple_vfio_region_write(VFIODevice *vbasedev, uint8_t nr, off_t off,
+                                   uint32_t size, void *data, bool post)
+{
+    io_connect_t conn = apple_vfio_connection(vbasedev);
+    uint64_t value = 0;
+    kern_return_t kr;
+    uint32_t legacy_size;
+
+    if (nr != VFIO_PCI_CONFIG_REGION_INDEX) {
+        return apple_vfio_bar_write(vbasedev, nr, off, size, data);
+    }
+
+    if (off < PCI_COMMAND + 2 && off + size > PCI_COMMAND) {
+        int ret = apple_vfio_forward_command_write(conn, off, size, data);
+
+        if (ret) {
+            return ret;
+        }
+
+        if (off >= PCI_COMMAND && off + size <= PCI_COMMAND + 2) {
+            return size;
+        }
+    }
+
+    if (!apple_vfio_config_write_is_safe(off, size)) {
+        return size;
+    }
+
+    if (conn == IO_OBJECT_NULL) {
+        return -ENODEV;
+    }
+
+    memcpy(&value, data, size);
+    legacy_size = MIN(size, PCIE_CONFIG_SPACE_SIZE - off);
+    if (!(legacy_size == 1 || legacy_size == 2 || legacy_size == 4)) {
+        return -EINVAL;
+    }
+
+    kr = apple_dext_config_write(conn, off, legacy_size, value);
+    if (kr != KERN_SUCCESS) {
+        return -EIO;
+    }
+    return size;
+}
+
+static int apple_vfio_region_map(VFIODevice *vbasedev, VFIORegion *region);
+static void apple_vfio_region_unmap(VFIODevice *vbasedev, VFIORegion *region);
+
+static int apple_vfio_region_map(VFIODevice *vbasedev, VFIORegion *region)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(vbasedev->dev);
+    VFIOPCIDevice *vdev = VFIO_PCI_DEVICE(adev);
+    io_connect_t conn = apple_vfio_connection(vbasedev);
+    int bar = region->nr;
+    VFIOBAR *vbar;
+    mach_vm_address_t local_addr = 0;
+    mach_vm_size_t bar_size = 0;
+    uint8_t bar_type = 0;
+    kern_return_t kr;
+    int i;
+
+    if (bar < VFIO_PCI_BAR0_REGION_INDEX || bar >= VFIO_PCI_ROM_REGION_INDEX) {
+        return 0;
+    }
+
+    vbar = &vdev->bars[bar];
+
+    if (conn == IO_OBJECT_NULL || !vbar->size || vbar->ioport) {
+        return 0;
+    }
+
+    if (bar > 0 && vdev->bars[bar - 1].mem64) {
+        return 0;
+    }
+
+    if (adev->apple->bar_maps[bar].addr != NULL) {
+        return 0;
+    }
+
+    kr = apple_dext_map_bar(conn, bar, &local_addr, &bar_size, &bar_type);
+    if (kr != KERN_SUCCESS) {
+        warn_report("vfio-apple: BAR%d map failed for %s: 0x%x",
+                    bar, vbasedev->name, kr);
+        return -EIO;
+    }
+
+    if (bar_size > vbar->size) {
+        bar_size = vbar->size;
+    }
+
+    adev->apple->bar_maps[bar].addr = (void *)local_addr;
+    adev->apple->bar_maps[bar].size = bar_size;
+
+    /*
+     * Use the pre-computed mmap regions — already split around the MSI-X
+     * table/PBA hole by vfio_pci_fixup_msix_region() during realize.
+     * We just need to fill in the host pointers from our dext mapping.
+     */
+    for (i = 0; i < region->nr_mmaps; i++) {
+        region->mmaps[i].mmap = (char *)local_addr + region->mmaps[i].offset;
+        vfio_region_register_mmap(region, i);
+    }
+
+    return 0;
+}
+
+static void apple_vfio_region_unmap(VFIODevice *vbasedev, VFIORegion *region)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(vbasedev->dev);
+    io_connect_t conn = apple_vfio_connection(vbasedev);
+    int bar = region->nr;
+    AppleVFIOBarMap *bm;
+    int i;
+
+    if (bar < VFIO_PCI_BAR0_REGION_INDEX || bar >= VFIO_PCI_ROM_REGION_INDEX) {
+        return;
+    }
+
+    bm = &adev->apple->bar_maps[bar];
+
+    for (i = 0; i < region->nr_mmaps; i++) {
+        if (region->mmaps[i].mmap) {
+            vfio_region_unregister_mmap(region, i);
+            region->mmaps[i].mmap = NULL;
+        }
+    }
+
+    if (conn != IO_OBJECT_NULL && bm->addr != NULL) {
+        apple_dext_unmap_bar(conn, bar, (mach_vm_address_t)bm->addr);
+    }
+
+    bm->addr = NULL;
+    bm->size = 0;
+}
+
+VFIODeviceIOOps apple_vfio_device_io_ops = {
+    .device_feature = apple_vfio_device_feature,
+    .get_region_info = apple_vfio_get_region_info,
+    .get_irq_info = apple_vfio_get_irq_info,
+    .set_irqs = apple_vfio_set_irqs,
+    .device_reset = apple_vfio_device_reset,
+    .region_read = apple_vfio_region_read,
+    .region_write = apple_vfio_region_write,
+    .region_map = apple_vfio_region_map,
+    .region_unmap = apple_vfio_region_unmap,
+};
+
+bool apple_vfio_dext_publish(uint8_t bus, uint8_t device, uint8_t function,
+                             io_connect_t conn)
+{
+    AppleVFIOSharedDext *shared;
+    guint key = apple_vfio_dext_key(bus, device, function);
+
+    if (!apple_vfio_shared_dexts) {
+        apple_vfio_shared_dexts =
+            g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
+    }
+
+    if (g_hash_table_lookup(apple_vfio_shared_dexts, GUINT_TO_POINTER(key))) {
+        return false;
+    }
+
+    shared = g_new0(AppleVFIOSharedDext, 1);
+    shared->conn = conn;
+    shared->refs = 1;
+    g_hash_table_insert(apple_vfio_shared_dexts, GUINT_TO_POINTER(key), shared);
+    return true;
+}
+
+io_connect_t apple_vfio_dext_lookup(uint8_t bus, uint8_t device,
+                                    uint8_t function)
+{
+    AppleVFIOSharedDext *shared;
+    guint key = apple_vfio_dext_key(bus, device, function);
+
+    if (!apple_vfio_shared_dexts) {
+        return IO_OBJECT_NULL;
+    }
+
+    shared = g_hash_table_lookup(apple_vfio_shared_dexts,
+                                 GUINT_TO_POINTER(key));
+    if (!shared) {
+        return IO_OBJECT_NULL;
+    }
+
+    shared->refs++;
+    return shared->conn;
+}
+
+void apple_vfio_dext_release(uint8_t bus, uint8_t device, uint8_t function,
+                             io_connect_t conn)
+{
+    AppleVFIOSharedDext *shared;
+    guint key = apple_vfio_dext_key(bus, device, function);
+
+    if (!apple_vfio_shared_dexts) {
+        return;
+    }
+
+    shared = g_hash_table_lookup(apple_vfio_shared_dexts,
+                                 GUINT_TO_POINTER(key));
+    if (!shared || shared->conn != conn) {
+        return;
+    }
+
+    if (--shared->refs == 0) {
+        apple_dext_disconnect(conn);
+        g_hash_table_remove(apple_vfio_shared_dexts, GUINT_TO_POINTER(key));
+    }
+}
+
+/* ------------------------------------------------------------------ */
+/* QOM type: vfio-apple-pci                                           */
+/* ------------------------------------------------------------------ */
+
+static void (*parent_realize)(PCIDevice *, Error **);
+static void (*parent_exit)(PCIDevice *);
+
+static void apple_vfio_pci_instance_init(Object *obj)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(obj);
+
+    apple_vfio_pci_init(adev);
+}
+
+static void apple_vfio_pci_realize_fn(PCIDevice *pdev, Error **errp)
+{
+    ERRP_GUARD();
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(pdev);
+
+    if (!apple_vfio_pci_pre_realize(adev, errp)) {
+        return;
+    }
+
+    parent_realize(pdev, errp);
+    if (*errp) {
+        g_clear_pointer(&adev->apple, g_free);
+        return;
+    }
+
+    if (!apple_vfio_create_dma_companion(adev, errp)) {
+        if (parent_exit) {
+            parent_exit(pdev);
+        }
+        g_clear_pointer(&adev->apple, g_free);
+        return;
+    }
+}
+
+static void apple_vfio_pci_exit_fn(PCIDevice *pdev)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(pdev);
+
+    apple_vfio_destroy_dma_companion(adev);
+
+    if (parent_exit) {
+        parent_exit(pdev);
+    }
+}
+
+static void apple_vfio_pci_finalize_fn(Object *obj)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(obj);
+
+    apple_vfio_device_cleanup(adev);
+    g_clear_pointer(&adev->apple, g_free);
+}
+
+static void apple_vfio_pci_class_init(ObjectClass *klass, const void *data)
+{
+    PCIDeviceClass *pdc = PCI_DEVICE_CLASS(klass);
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    parent_realize = pdc->realize;
+    parent_exit = pdc->exit;
+
+    pdc->realize = apple_vfio_pci_realize_fn;
+    pdc->exit = apple_vfio_pci_exit_fn;
+    dc->user_creatable = true;
+    dc->desc = "VFIO-based PCI device assignment (Apple/macOS)";
+}
+
+static const TypeInfo vfio_apple_pci_info = {
+    .name = TYPE_VFIO_APPLE_PCI,
+    .parent = TYPE_VFIO_PCI,
+    .instance_size = sizeof(VFIOApplePCIDevice),
+    .instance_init = apple_vfio_pci_instance_init,
+    .instance_finalize = apple_vfio_pci_finalize_fn,
+    .class_init = apple_vfio_pci_class_init,
+};
+
+static void register_vfio_apple_pci_type(void)
+{
+    type_register_static(&vfio_apple_pci_info);
+}
+
+type_init(register_vfio_apple_pci_type)
diff --git a/hw/vfio/apple.h b/hw/vfio/apple.h
new file mode 100644
index 0000000000..81d4bd2b66
--- /dev/null
+++ b/hw/vfio/apple.h
@@ -0,0 +1,74 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Apple/macOS VFIO passthrough common definitions.
+ *
+ * Copyright (c) 2026 Scott J. Goldman
+ */
+
+#ifndef HW_VFIO_APPLE_H
+#define HW_VFIO_APPLE_H
+
+#include <stdint.h>
+
+#include "hw/vfio/pci.h"
+#include "hw/vfio/vfio-container.h"
+#include "qapi/error.h"
+#include "qemu/event_notifier.h"
+
+#ifdef CONFIG_DARWIN
+#include <IOKit/IOKitLib.h>
+#else
+typedef uintptr_t io_connect_t;
+#define IO_OBJECT_NULL ((io_connect_t)0)
+#endif
+
+OBJECT_DECLARE_SIMPLE_TYPE(AppleVFIOContainer, VFIO_IOMMU_APPLE)
+
+struct AppleVFIOContainer {
+    VFIOContainer parent_obj;
+    io_connect_t dext_conn;
+    uint8_t host_bus;
+    uint8_t host_device;
+    uint8_t host_function;
+};
+
+typedef struct AppleDextInterruptNotify AppleDextInterruptNotify;
+
+typedef struct AppleVFIOBarMap {
+    void *addr;
+    size_t size;
+} AppleVFIOBarMap;
+
+typedef struct AppleVFIOState {
+    AppleDextInterruptNotify *irq_notify;
+    EventNotifier irq_notifier;
+    uint32_t num_irq_vectors;
+    AppleVFIOBarMap bar_maps[PCI_ROM_SLOT];
+} AppleVFIOState;
+
+OBJECT_DECLARE_SIMPLE_TYPE(VFIOApplePCIDevice, VFIO_APPLE_PCI)
+
+struct VFIOApplePCIDevice {
+    VFIOPCIDevice parent_obj;
+    AppleVFIOState *apple;
+    DeviceState *dma_companion;
+    bool dma_companion_autocreated;
+};
+
+extern VFIODeviceIOOps apple_vfio_device_io_ops;
+
+bool apple_vfio_device_setup(VFIOApplePCIDevice *adev, Error **errp);
+void apple_vfio_device_cleanup(VFIOApplePCIDevice *adev);
+bool apple_vfio_get_bar_info(VFIOApplePCIDevice *adev, uint8_t bar,
+                             uint8_t *mem_idx, uint64_t *size,
+                             uint8_t *type);
+
+bool apple_vfio_dext_publish(uint8_t bus, uint8_t device, uint8_t function,
+                             io_connect_t conn);
+io_connect_t apple_vfio_dext_lookup(uint8_t bus, uint8_t device,
+                                    uint8_t function);
+void apple_vfio_dext_release(uint8_t bus, uint8_t device, uint8_t function,
+                             io_connect_t conn);
+
+#endif /* HW_VFIO_APPLE_H */
diff --git a/hw/vfio/container-apple.c b/hw/vfio/container-apple.c
new file mode 100644
index 0000000000..5a5c55b622
--- /dev/null
+++ b/hw/vfio/container-apple.c
@@ -0,0 +1,241 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Apple/macOS VFIO IOMMU container backend.
+ *
+ * Copyright (c) 2026 Scott J. Goldman
+ */
+
+#include "qemu/osdep.h"
+
+#include <linux/vfio.h>
+
+#include "apple-dext-client.h"
+#include "hw/vfio/apple.h"
+#include "hw/vfio/vfio-device.h"
+#include "hw/vfio/vfio-listener.h"
+#include "qapi/error.h"
+#include "system/ramblock.h"
+
+static bool apple_vfio_setup(VFIOContainer *bcontainer, Error **errp)
+{
+    bcontainer->pgsizes = qemu_real_host_page_size();
+    bcontainer->dma_max_mappings = UINT_MAX;
+    bcontainer->dirty_pages_supported = false;
+    bcontainer->max_dirty_bitmap_size = 0;
+    bcontainer->dirty_pgsizes = 0;
+    return true;
+}
+
+/*
+ * DMA map/unmap are no-ops: Apple passthrough handles DMA mapping through
+ * the companion apple-dma-pci device which talks to the dext directly,
+ * bypassing the IOMMU container's DMA path.  The stubs are required because
+ * the VFIO listener asserts they are non-NULL.
+ */
+static int apple_vfio_dma_map(const VFIOContainer *bcontainer, hwaddr iova,
+                              uint64_t size, void *vaddr, bool readonly,
+                              MemoryRegion *mr)
+{
+    return 0;
+}
+
+static int apple_vfio_dma_unmap(const VFIOContainer *bcontainer, hwaddr iova,
+                                uint64_t size, IOMMUTLBEntry *iotlb,
+                                bool unmap_all)
+{
+    return 0;
+}
+
+static int apple_vfio_set_dirty_page_tracking(const VFIOContainer *bcontainer,
+                                              bool start, Error **errp)
+{
+    error_setg_errno(errp, ENOTSUP, "vfio-apple does not support migration");
+    return -ENOTSUP;
+}
+
+static int apple_vfio_query_dirty_bitmap(const VFIOContainer *bcontainer,
+                                         VFIOBitmap *vbmap, hwaddr iova,
+                                         hwaddr size, uint64_t backend_flag,
+                                         Error **errp)
+{
+    error_setg_errno(errp, ENOTSUP, "vfio-apple does not support migration");
+    return -ENOTSUP;
+}
+
+static int apple_vfio_pci_hot_reset(VFIODevice *vbasedev, bool single)
+{
+    return 0;
+}
+
+static AppleVFIOContainer *apple_vfio_container_connect(AddressSpace *as,
+                                                        VFIODevice *vbasedev,
+                                                        Error **errp)
+{
+    VFIOPCIDevice *vdev = VFIO_PCI_DEVICE(vbasedev->dev);
+    AppleVFIOContainer *container;
+    VFIOContainer *bcontainer;
+    VFIOAddressSpace *space;
+    VFIOIOMMUClass *vioc;
+    int ret;
+
+    space = vfio_address_space_get(as);
+    container = VFIO_IOMMU_APPLE(object_new(TYPE_VFIO_IOMMU_APPLE));
+    bcontainer = VFIO_IOMMU(container);
+    vioc = VFIO_IOMMU_GET_CLASS(bcontainer);
+
+    container->host_bus = vdev->host.bus;
+    container->host_device = vdev->host.slot;
+    container->host_function = vdev->host.function;
+
+    ret = ram_block_uncoordinated_discard_disable(true);
+    if (ret) {
+        error_setg_errno(errp, -ret, "Cannot set discarding of RAM broken");
+        goto fail_unref;
+    }
+
+    container->dext_conn = apple_dext_connect(container->host_bus,
+                                                  container->host_device,
+                                                  container->host_function);
+    if (container->dext_conn == IO_OBJECT_NULL) {
+        error_setg(errp,
+                   "vfio-apple: could not connect to dext for host PCI "
+                   "%02x:%02x.%x",
+                   container->host_bus, container->host_device,
+                   container->host_function);
+        goto fail_discards;
+    }
+
+    if (apple_dext_claim(container->dext_conn) != KERN_SUCCESS) {
+        error_setg(errp,
+                   "vfio-apple: failed to claim dext-backed PCI device "
+                   "%02x:%02x.%x",
+                   container->host_bus, container->host_device,
+                   container->host_function);
+        goto fail_release_conn;
+    }
+
+    if (!apple_vfio_dext_publish(container->host_bus, container->host_device,
+                                 container->host_function,
+                                 container->dext_conn)) {
+        error_setg(errp,
+                   "vfio-apple: duplicate dext owner for host PCI %02x:%02x.%x",
+                   container->host_bus, container->host_device,
+                   container->host_function);
+        goto fail_release_conn;
+    }
+
+    if (!vioc->setup(bcontainer, errp)) {
+        goto fail_shared_conn;
+    }
+
+    vfio_address_space_insert(space, bcontainer);
+
+    if (!vfio_listener_register(bcontainer, errp)) {
+        goto fail_address_space;
+    }
+
+    bcontainer->initialized = true;
+    return container;
+
+fail_address_space:
+    vfio_listener_unregister(bcontainer);
+    QLIST_REMOVE(bcontainer, next);
+    bcontainer->space = NULL;
+fail_shared_conn:
+    apple_vfio_dext_release(container->host_bus, container->host_device,
+                            container->host_function, container->dext_conn);
+    container->dext_conn = IO_OBJECT_NULL;
+fail_discards:
+    ram_block_uncoordinated_discard_disable(false);
+fail_unref:
+    object_unref(container);
+    vfio_address_space_put(space);
+    return NULL;
+
+fail_release_conn:
+    apple_dext_disconnect(container->dext_conn);
+    container->dext_conn = IO_OBJECT_NULL;
+    goto fail_discards;
+}
+
+static void apple_vfio_container_disconnect(AppleVFIOContainer *container)
+{
+    VFIOContainer *bcontainer = VFIO_IOMMU(container);
+    VFIOAddressSpace *space = bcontainer->space;
+
+    ram_block_uncoordinated_discard_disable(false);
+    vfio_listener_unregister(bcontainer);
+
+    apple_vfio_dext_release(container->host_bus, container->host_device,
+                            container->host_function, container->dext_conn);
+    container->dext_conn = IO_OBJECT_NULL;
+
+    object_unref(container);
+    vfio_address_space_put(space);
+}
+
+static bool apple_vfio_attach_device(const char *name, VFIODevice *vbasedev,
+                                     AddressSpace *as, Error **errp)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(vbasedev->dev);
+    AppleVFIOContainer *container;
+    struct vfio_device_info info = {
+        .argsz = sizeof(info),
+        .flags = VFIO_DEVICE_FLAGS_PCI | VFIO_DEVICE_FLAGS_RESET,
+        .num_regions = VFIO_PCI_NUM_REGIONS,
+        .num_irqs = VFIO_PCI_NUM_IRQS,
+    };
+
+    container = apple_vfio_container_connect(as, vbasedev, errp);
+    if (!container) {
+        return false;
+    }
+
+    vbasedev->fd = -1;
+    vbasedev->io_ops = &apple_vfio_device_io_ops;
+    vfio_device_prepare(vbasedev, VFIO_IOMMU(container), &info);
+
+    if (!apple_vfio_device_setup(adev, errp)) {
+        vfio_device_unprepare(vbasedev);
+        apple_vfio_container_disconnect(container);
+        return false;
+    }
+
+    return true;
+}
+
+static void apple_vfio_detach_device(VFIODevice *vbasedev)
+{
+    VFIOApplePCIDevice *adev = VFIO_APPLE_PCI(vbasedev->dev);
+    AppleVFIOContainer *container = VFIO_IOMMU_APPLE(vbasedev->bcontainer);
+
+    apple_vfio_device_cleanup(adev);
+    vfio_device_unprepare(vbasedev);
+    apple_vfio_container_disconnect(container);
+}
+
+static void vfio_iommu_apple_class_init(ObjectClass *klass, const void *data)
+{
+    VFIOIOMMUClass *vioc = VFIO_IOMMU_CLASS(klass);
+
+    vioc->setup = apple_vfio_setup;
+    vioc->dma_map = apple_vfio_dma_map;
+    vioc->dma_unmap = apple_vfio_dma_unmap;
+    vioc->attach_device = apple_vfio_attach_device;
+    vioc->detach_device = apple_vfio_detach_device;
+    vioc->set_dirty_page_tracking = apple_vfio_set_dirty_page_tracking;
+    vioc->query_dirty_bitmap = apple_vfio_query_dirty_bitmap;
+    vioc->pci_hot_reset = apple_vfio_pci_hot_reset;
+}
+
+static const TypeInfo apple_vfio_types[] = {
+    {
+        .name = TYPE_VFIO_IOMMU_APPLE,
+        .parent = TYPE_VFIO_IOMMU,
+        .instance_size = sizeof(AppleVFIOContainer),
+        .class_init = vfio_iommu_apple_class_init,
+    },
+};
+
+DEFINE_TYPES(apple_vfio_types)
diff --git a/hw/vfio/meson.build b/hw/vfio/meson.build
index 965c8e5b80..473f8669f9 100644
--- a/hw/vfio/meson.build
+++ b/hw/vfio/meson.build
@@ -40,6 +40,8 @@ system_ss.add(when: 'CONFIG_VFIO_PCI', if_true: files(
 # Apple VFIO backend
 if host_os == 'darwin'
   system_ss.add(when: 'CONFIG_VFIO',
-                if_true: [files('apple-dext-client.c'),
-                coref, iokit])
+                if_true: [files('apple-device.c',
+                                'container-apple.c',
+                                'apple-dext-client.c'),
+                          coref, iokit])
 endif
-- 
2.50.1 (Apple Git-155)