[RFC PATCH 07/10] vfio/apple: Add DriverKit dext client library

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 07/10] vfio/apple: Add DriverKit dext client library
Posted by Scott J. Goldman 6 days, 11 hours ago
From: "Scott J. Goldman" <scottjg@umich.edu>

Add the C client library for communicating with the VFIOUserPCIDriver
DriverKit extension (dext) on macOS.  This provides the low-level
IOUserClient wrappers that the Apple VFIO backend will use:

- Connection management (connect, disconnect, claim)
- PCI config space read/write (individual and block)
- BAR info queries, BAR mapping/unmapping, MMIO read/write
- DMA region registration and unregistration
- Interrupt setup, pending IRQ polling, async notification
- Device reset (FLR with hot-reset fallback)

All calls go through IOKit's IOConnectCallScalarMethod /
IOConnectMapMemory64 to the dext, which mediates access to the
physical PCI device via PCIDriverKit.

Signed-off-by: Scott J. Goldman <scottjgo@gmail.com>
---
 hw/vfio/apple-dext-client.c | 681 ++++++++++++++++++++++++++++++++++++
 hw/vfio/apple-dext-client.h | 253 ++++++++++++++
 hw/vfio/meson.build         |   7 +
 3 files changed, 941 insertions(+)
 create mode 100644 hw/vfio/apple-dext-client.c
 create mode 100644 hw/vfio/apple-dext-client.h

diff --git a/hw/vfio/apple-dext-client.c b/hw/vfio/apple-dext-client.c
new file mode 100644
index 0000000000..7ba03fc6e9
--- /dev/null
+++ b/hw/vfio/apple-dext-client.c
@@ -0,0 +1,681 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * C client implementation for communicating with the VFIOUserPCIDriver dext
+ * via IOKit IOUserClient.
+ *
+ * Copyright (c) 2026 Scott J. Goldman
+ */
+
+#include "qemu/osdep.h"
+
+#include "apple-dext-client.h"
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <IOKit/IOKitLib.h>
+#include <dispatch/dispatch.h>
+#include <string.h>
+
+enum {
+    kSelectorGetIdentity        = 0,
+    kSelectorClaim              = 1,
+    kSelectorTerminate          = 2,
+    kSelectorAllocateDMABuffer  = 3,
+    kSelectorFreeDMABuffer      = 4,
+    kSelectorRegisterDMARegion  = 5,
+    kSelectorUnregisterDMARegion = 6,
+    kSelectorProbeDMARegion     = 7,
+    kSelectorConfigRead         = 8,
+    kSelectorConfigWrite        = 9,
+    kSelectorGetBARInfo         = 10,
+    kSelectorMMIORead           = 11,
+    kSelectorMMIOWrite          = 12,
+    kSelectorSetupInterrupts    = 13,
+    kSelectorCheckInterrupt     = 14,
+    kSelectorWaitInterrupt      = 15,
+    kSelectorSetIRQMask         = 16,
+    kSelectorResetDevice        = 17,
+};
+
+/*
+ * Keep this in sync with PCIDriverKit BAR type encoding. Bit 3 indicates
+ * prefetchability for memory BARs.
+ */
+#define APPLE_DEXT_BAR_PREFETCHABLE_MASK 0x08
+#ifndef kIOMapWriteCombineCache
+#define kIOMapWriteCombineCache 0x00000400
+#endif
+
+static bool
+dext_service_matches_class(io_service_t service, const char *className)
+{
+    bool match = false;
+    CFTypeRef ref;
+
+    ref = IORegistryEntryCreateCFProperty(service, CFSTR("IOUserClass"),
+                                          kCFAllocatorDefault, 0);
+    if (ref == NULL) {
+        return false;
+    }
+
+    if (CFGetTypeID(ref) == CFStringGetTypeID()) {
+        CFStringRef expected = CFStringCreateWithCString(
+            kCFAllocatorDefault, className, kCFStringEncodingUTF8);
+        if (expected != NULL) {
+            match = CFStringCompare((CFStringRef)ref, expected, 0)
+                    == kCFCompareEqualTo;
+            CFRelease(expected);
+        }
+    }
+    CFRelease(ref);
+    return match;
+}
+
+static bool
+dext_connection_matches_bdf(io_connect_t connection,
+                            uint8_t bus, uint8_t device, uint8_t function)
+{
+    uint64_t output[6] = {0};
+    uint32_t outputCount = 6;
+    kern_return_t kr;
+
+    kr = IOConnectCallMethod(connection, kSelectorGetIdentity,
+                             NULL, 0, NULL, 0,
+                             output, &outputCount,
+                             NULL, NULL);
+    if (kr != KERN_SUCCESS || outputCount < 3) {
+        return false;
+    }
+
+    return (uint8_t)output[0] == bus &&
+           (uint8_t)output[1] == device &&
+           (uint8_t)output[2] == function;
+}
+
+io_connect_t
+apple_dext_connect(uint8_t bus, uint8_t device, uint8_t function)
+{
+    CFMutableDictionaryRef matching;
+    io_iterator_t iterator = IO_OBJECT_NULL;
+    io_connect_t result = IO_OBJECT_NULL;
+    io_service_t service;
+    kern_return_t kr;
+
+    matching = IOServiceMatching("IOUserService");
+    if (matching == NULL) {
+        return IO_OBJECT_NULL;
+    }
+
+    kr = IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator);
+    if (kr != KERN_SUCCESS) {
+        return IO_OBJECT_NULL;
+    }
+
+    while ((service = IOIteratorNext(iterator)) != IO_OBJECT_NULL) {
+        io_connect_t connection = IO_OBJECT_NULL;
+
+        if (!dext_service_matches_class(service, "VFIOUserPCIDriver")) {
+            IOObjectRelease(service);
+            continue;
+        }
+
+        kr = IOServiceOpen(service, mach_task_self(), 0, &connection);
+        IOObjectRelease(service);
+
+        if (kr != KERN_SUCCESS) {
+            continue;
+        }
+
+        if (dext_connection_matches_bdf(connection, bus, device, function)) {
+            result = connection;
+            break;
+        }
+
+        IOServiceClose(connection);
+    }
+
+    IOObjectRelease(iterator);
+    return result;
+}
+
+void
+apple_dext_disconnect(io_connect_t connection)
+{
+    if (connection != IO_OBJECT_NULL) {
+        IOServiceClose(connection);
+    }
+}
+
+kern_return_t
+apple_dext_claim(io_connect_t connection)
+{
+    if (connection == IO_OBJECT_NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    return IOConnectCallMethod(connection,
+                               kSelectorClaim,
+                               NULL, 0, NULL, 0,
+                               NULL, NULL, NULL, NULL);
+}
+
+kern_return_t
+apple_dext_register_dma(io_connect_t connection,
+                            uint64_t iova,
+                            uint64_t client_va,
+                            uint64_t size,
+                            uint64_t *out_bus_addr,
+                            uint64_t *out_bus_len)
+{
+    uint64_t input[3] = { iova, client_va, size };
+    uint64_t output[3] = {0};
+    uint32_t outputCount = 3;
+    kern_return_t kr;
+
+    if (connection == IO_OBJECT_NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    kr = IOConnectCallMethod(connection,
+                             kSelectorRegisterDMARegion,
+                             input, 3,
+                             NULL, 0,
+                             output, &outputCount,
+                             NULL, NULL);
+    if (kr != KERN_SUCCESS) {
+        return kr;
+    }
+
+    if (out_bus_addr != NULL && outputCount >= 2) {
+        *out_bus_addr = output[1];
+    }
+    if (out_bus_len != NULL && outputCount >= 3) {
+        *out_bus_len = output[2];
+    }
+
+    return kIOReturnSuccess;
+}
+
+kern_return_t
+apple_dext_unregister_dma(io_connect_t connection,
+                              uint64_t iova)
+{
+    uint64_t input[1] = { iova };
+
+    if (connection == IO_OBJECT_NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    return IOConnectCallMethod(connection,
+                               kSelectorUnregisterDMARegion,
+                               input, 1,
+                               NULL, 0,
+                               NULL, NULL,
+                               NULL, NULL);
+}
+
+kern_return_t
+apple_dext_probe_dma(io_connect_t connection,
+                         uint64_t iova,
+                         uint64_t offset,
+                         uint64_t *out_word)
+{
+    uint64_t input[2] = { iova, offset };
+    uint64_t output[1] = {0};
+    uint32_t outputCount = 1;
+    kern_return_t kr;
+
+    if (connection == IO_OBJECT_NULL || out_word == NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    kr = IOConnectCallMethod(connection,
+                             kSelectorProbeDMARegion,
+                             input, 2,
+                             NULL, 0,
+                             output, &outputCount,
+                             NULL, NULL);
+    if (kr != KERN_SUCCESS) {
+        return kr;
+    }
+
+    *out_word = output[0];
+    return kIOReturnSuccess;
+}
+
+kern_return_t
+apple_dext_config_read(io_connect_t connection,
+                           uint64_t offset,
+                           uint64_t width,
+                           uint64_t *out_value)
+{
+    uint64_t input[2] = { offset, width };
+    uint64_t output[1] = {0};
+    uint32_t outputCount = 1;
+    kern_return_t kr;
+
+    if (connection == IO_OBJECT_NULL || out_value == NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    kr = IOConnectCallMethod(connection,
+                             kSelectorConfigRead,
+                             input, 2,
+                             NULL, 0,
+                             output, &outputCount,
+                             NULL, NULL);
+    if (kr != KERN_SUCCESS) {
+        return kr;
+    }
+
+    *out_value = output[0];
+    return kIOReturnSuccess;
+}
+
+kern_return_t
+apple_dext_config_write(io_connect_t connection,
+                            uint64_t offset,
+                            uint64_t width,
+                            uint64_t value)
+{
+    uint64_t input[3] = { offset, width, value };
+
+    if (connection == IO_OBJECT_NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    return IOConnectCallMethod(connection,
+                               kSelectorConfigWrite,
+                               input, 3,
+                               NULL, 0,
+                               NULL, NULL,
+                               NULL, NULL);
+}
+
+kern_return_t
+apple_dext_config_read_block(io_connect_t connection,
+                                 uint64_t offset,
+                                 void *buf,
+                                 size_t len)
+{
+    uint8_t *dst = (uint8_t *)buf;
+    uint64_t pos = offset;
+    size_t remaining = len;
+
+    if (connection == IO_OBJECT_NULL || buf == NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    while (remaining >= 4) {
+        uint64_t val = 0;
+        uint32_t dword;
+        kern_return_t kr;
+
+        kr = apple_dext_config_read(connection, pos, 4, &val);
+        if (kr != KERN_SUCCESS) {
+            return kr;
+        }
+        dword = (uint32_t)val;
+        memcpy(dst, &dword, 4);
+        dst += 4;
+        pos += 4;
+        remaining -= 4;
+    }
+
+    while (remaining > 0) {
+        uint64_t val = 0;
+        kern_return_t kr;
+
+        kr = apple_dext_config_read(connection, pos, 1, &val);
+        if (kr != KERN_SUCCESS) {
+            return kr;
+        }
+        *dst = (uint8_t)val;
+        dst++;
+        pos++;
+        remaining--;
+    }
+
+    return kIOReturnSuccess;
+}
+
+kern_return_t
+apple_dext_get_bar_info(io_connect_t connection,
+                            uint8_t bar,
+                            uint8_t *out_mem_idx,
+                            uint64_t *out_size,
+                            uint8_t *out_type)
+{
+    uint64_t input[1] = { bar };
+    uint64_t output[3] = {0};
+    uint32_t outputCount = 3;
+    kern_return_t kr;
+
+    if (connection == IO_OBJECT_NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    kr = IOConnectCallMethod(connection,
+                             kSelectorGetBARInfo,
+                             input, 1,
+                             NULL, 0,
+                             output, &outputCount,
+                             NULL, NULL);
+    if (kr != KERN_SUCCESS) {
+        return kr;
+    }
+
+    if (out_mem_idx != NULL) {
+        *out_mem_idx = (uint8_t)output[0];
+    }
+    if (out_size != NULL) {
+        *out_size = output[1];
+    }
+    if (out_type != NULL) {
+        *out_type = (uint8_t)output[2];
+    }
+
+    return kIOReturnSuccess;
+}
+
+kern_return_t
+apple_dext_map_bar(io_connect_t connection,
+                       uint8_t bar,
+                       mach_vm_address_t *out_addr,
+                       mach_vm_size_t *out_size,
+                       uint8_t *out_type)
+{
+    uint64_t bar_size = 0;
+    uint8_t bar_type = 0;
+    uint32_t mem_type;
+    mach_vm_address_t addr = 0;
+    mach_vm_size_t size = 0;
+    IOOptionBits opts = kIOMapAnywhere;
+    kern_return_t kr;
+
+    if (connection == IO_OBJECT_NULL || out_addr == NULL || out_size == NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    kr = apple_dext_get_bar_info(connection, bar, NULL,
+                                     &bar_size, &bar_type);
+    if (kr != KERN_SUCCESS) {
+        return kr;
+    }
+
+    /*
+     * The memory type for IOConnectMapMemory64 must match the dext's
+     * CopyClientMemoryForType expectation:
+     * kVFIOUserPCIDriverUserClientMemoryTypeBAR0 (= 1) plus the BAR index.
+     * This is NOT the same as the PCIDriverKit internal memoryIndex returned
+     * by GetBARInfo.
+     */
+    mem_type = 1 + (uint32_t)bar;
+
+    if (bar_type & APPLE_DEXT_BAR_PREFETCHABLE_MASK) {
+        opts |= kIOMapWriteCombineCache;
+    }
+
+    kr = IOConnectMapMemory64(connection, mem_type, mach_task_self(),
+                              &addr, &size, opts);
+    if (kr != KERN_SUCCESS) {
+        return kr;
+    }
+
+    *out_addr = addr;
+    *out_size = size;
+    if (out_type != NULL) {
+        *out_type = bar_type;
+    }
+    return kIOReturnSuccess;
+}
+
+kern_return_t
+apple_dext_unmap_bar(io_connect_t connection,
+                         uint8_t bar,
+                         mach_vm_address_t addr)
+{
+    uint32_t mem_type = 1 + (uint32_t)bar;
+
+    if (connection == IO_OBJECT_NULL || addr == 0) {
+        return kIOReturnBadArgument;
+    }
+
+    return IOConnectUnmapMemory64(connection, mem_type, mach_task_self(), addr);
+}
+
+kern_return_t
+apple_dext_mmio_read(io_connect_t connection,
+                         uint8_t mem_idx,
+                         uint64_t offset,
+                         uint64_t width,
+                         uint64_t *out_value)
+{
+    uint64_t input[3] = { mem_idx, offset, width };
+    uint64_t output[1] = {0};
+    uint32_t outputCount = 1;
+    kern_return_t kr;
+
+    if (connection == IO_OBJECT_NULL || out_value == NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    kr = IOConnectCallMethod(connection,
+                             kSelectorMMIORead,
+                             input, 3,
+                             NULL, 0,
+                             output, &outputCount,
+                             NULL, NULL);
+    if (kr != KERN_SUCCESS) {
+        return kr;
+    }
+
+    *out_value = output[0];
+    return kIOReturnSuccess;
+}
+
+kern_return_t
+apple_dext_mmio_write(io_connect_t connection,
+                          uint8_t mem_idx,
+                          uint64_t offset,
+                          uint64_t width,
+                          uint64_t value)
+{
+    uint64_t input[4] = { mem_idx, offset, width, value };
+
+    if (connection == IO_OBJECT_NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    return IOConnectCallMethod(connection,
+                               kSelectorMMIOWrite,
+                               input, 4,
+                               NULL, 0,
+                               NULL, NULL,
+                               NULL, NULL);
+}
+
+kern_return_t
+apple_dext_setup_interrupts(io_connect_t connection,
+                                uint32_t *out_num_vectors)
+{
+    uint64_t output[1] = {0};
+    uint32_t outputCount = 1;
+    kern_return_t kr;
+
+    if (connection == IO_OBJECT_NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    kr = IOConnectCallMethod(connection,
+                             kSelectorSetupInterrupts,
+                             NULL, 0,
+                             NULL, 0,
+                             output, &outputCount,
+                             NULL, NULL);
+    if (kr != KERN_SUCCESS) {
+        return kr;
+    }
+
+    if (out_num_vectors != NULL && outputCount >= 1) {
+        *out_num_vectors = (uint32_t)output[0];
+    }
+
+    return kIOReturnSuccess;
+}
+
+kern_return_t
+apple_dext_reset_device(io_connect_t connection)
+{
+    if (connection == IO_OBJECT_NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    return IOConnectCallMethod(connection,
+                               kSelectorResetDevice,
+                               NULL, 0, NULL, 0,
+                               NULL, NULL, NULL, NULL);
+}
+
+kern_return_t
+apple_dext_set_irq_mask(io_connect_t connection, const uint64_t mask[4])
+{
+    if (connection == IO_OBJECT_NULL || mask == NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    return IOConnectCallMethod(connection,
+                               kSelectorSetIRQMask,
+                               mask, 4,
+                               NULL, 0,
+                               NULL, NULL,
+                               NULL, NULL);
+}
+
+kern_return_t
+apple_dext_read_pending_irqs(io_connect_t connection, uint64_t pending[4])
+{
+    uint64_t output[4] = {0};
+    uint32_t outputCount = 4;
+    kern_return_t kr;
+    uint32_t i;
+
+    if (connection == IO_OBJECT_NULL || pending == NULL) {
+        return kIOReturnBadArgument;
+    }
+
+    kr = IOConnectCallMethod(connection,
+                             kSelectorCheckInterrupt,
+                             NULL, 0,
+                             NULL, 0,
+                             output, &outputCount,
+                             NULL, NULL);
+    if (kr != KERN_SUCCESS) {
+        return kr;
+    }
+
+    for (i = 0; i < 4; i++) {
+        pending[i] = (i < outputCount) ? output[i] : 0;
+    }
+
+    return kIOReturnSuccess;
+}
+
+struct AppleDextInterruptNotify {
+    io_connect_t connection;
+    IONotificationPortRef notifyPort;
+    mach_port_t machPort;
+    dispatch_queue_t dispatchQueue;
+    void (*handler_fn)(void *opaque);
+    void *opaque;
+};
+
+static void
+apple_dext_async_callback(void *refcon, IOReturn result,
+                          void **args, uint32_t numArgs)
+{
+    AppleDextInterruptNotify *notify = refcon;
+
+    if (result == kIOReturnSuccess && notify->handler_fn) {
+        notify->handler_fn(notify->opaque);
+    }
+}
+
+static kern_return_t
+apple_dext_interrupt_notify_arm(AppleDextInterruptNotify *notify)
+{
+    uint64_t asyncRef[kIOAsyncCalloutCount];
+
+    asyncRef[kIOAsyncCalloutFuncIndex] =
+        (uint64_t)(uintptr_t)apple_dext_async_callback;
+    asyncRef[kIOAsyncCalloutRefconIndex] =
+        (uint64_t)(uintptr_t)notify;
+
+    return IOConnectCallAsyncMethod(notify->connection,
+                                    kSelectorWaitInterrupt,
+                                    notify->machPort,
+                                    asyncRef, kIOAsyncCalloutCount,
+                                    NULL, 0, NULL, 0,
+                                    NULL, NULL, NULL, NULL);
+}
+
+AppleDextInterruptNotify *
+apple_dext_interrupt_notify_create(io_connect_t connection,
+                                   void (*handler_fn)(void *opaque),
+                                   void *opaque)
+{
+    AppleDextInterruptNotify *notify;
+    kern_return_t kr;
+
+    if (connection == IO_OBJECT_NULL || handler_fn == NULL) {
+        return NULL;
+    }
+
+    notify = g_new0(AppleDextInterruptNotify, 1);
+    notify->connection = connection;
+    notify->handler_fn = handler_fn;
+    notify->opaque = opaque;
+
+    notify->notifyPort = IONotificationPortCreate(kIOMainPortDefault);
+    if (!notify->notifyPort) {
+        g_free(notify);
+        return NULL;
+    }
+
+    notify->dispatchQueue = dispatch_queue_create(
+        "org.qemu.vfio-apple.irq-notify", DISPATCH_QUEUE_SERIAL);
+    IONotificationPortSetDispatchQueue(notify->notifyPort,
+                                       notify->dispatchQueue);
+    notify->machPort = IONotificationPortGetMachPort(notify->notifyPort);
+
+    kr = apple_dext_interrupt_notify_arm(notify);
+    if (kr != KERN_SUCCESS) {
+        IONotificationPortDestroy(notify->notifyPort);
+        dispatch_release(notify->dispatchQueue);
+        g_free(notify);
+        return NULL;
+    }
+
+    return notify;
+}
+
+kern_return_t
+apple_dext_interrupt_notify_rearm(AppleDextInterruptNotify *notify)
+{
+    if (!notify) {
+        return kIOReturnBadArgument;
+    }
+    return apple_dext_interrupt_notify_arm(notify);
+}
+
+void
+apple_dext_interrupt_notify_destroy(AppleDextInterruptNotify *notify)
+{
+    if (!notify) {
+        return;
+    }
+
+    IONotificationPortDestroy(notify->notifyPort);
+    dispatch_release(notify->dispatchQueue);
+    g_free(notify);
+}
diff --git a/hw/vfio/apple-dext-client.h b/hw/vfio/apple-dext-client.h
new file mode 100644
index 0000000000..07574493e6
--- /dev/null
+++ b/hw/vfio/apple-dext-client.h
@@ -0,0 +1,253 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * C API for connecting to the VFIOUserPCIDriver DriverKit extension.
+ *
+ * The vfio-user server process uses this to:
+ *  1. Find and open an IOUserClient to the dext for a given PCI BDF.
+ *  2. Claim the device so the dext opens its IOPCIDevice provider.
+ *  3. Register client-owned memory (QEMU guest RAM mapped via shared file)
+ *     for DMA by the physical PCI device.
+ *  4. Unregister DMA regions when QEMU removes them.
+ *
+ * Integration with libvfio-user:
+ *   vfu_dma_register_cb_t  ->  apple_dext_register_dma()
+ *   vfu_dma_unregister_cb_t -> apple_dext_unregister_dma()
+ *
+ * Copyright (c) 2026 Scott J. Goldman
+ */
+
+#ifndef HW_VFIO_APPLE_DEXT_CLIENT_H
+#define HW_VFIO_APPLE_DEXT_CLIENT_H
+
+#include <IOKit/IOKitLib.h>
+#include <stdint.h>
+
+/*
+ * Find the VFIOUserPCIDriver dext instance matching the given PCI BDF
+ * and open an IOUserClient connection to it.
+ * Returns IO_OBJECT_NULL on failure.
+ */
+io_connect_t apple_dext_connect(uint8_t bus, uint8_t device,
+                                    uint8_t function);
+
+/*
+ * Close a previously opened connection.
+ */
+void apple_dext_disconnect(io_connect_t connection);
+
+/*
+ * Claim the PCI device through the dext (opens the IOPCIDevice provider).
+ * Must be called before registering DMA regions.
+ */
+kern_return_t apple_dext_claim(io_connect_t connection);
+
+/*
+ * Register a region of this process's address space for DMA.
+ *
+ * @iova:         guest IOVA (device-visible DMA address)
+ * @client_va:    virtual address of the memory in this process
+ * @size:         region size in bytes
+ * @out_bus_addr: receives first DMA bus address segment (may be NULL)
+ * @out_bus_len:  receives first DMA bus address segment length (may be NULL)
+ *
+ * The memory at client_va must remain valid and mapped until the region
+ * is unregistered.
+ */
+kern_return_t apple_dext_register_dma(io_connect_t connection,
+                                          uint64_t iova,
+                                          uint64_t client_va,
+                                          uint64_t size,
+                                          uint64_t *out_bus_addr,
+                                          uint64_t *out_bus_len);
+
+/*
+ * Unregister a previously registered DMA region identified by its IOVA.
+ */
+kern_return_t apple_dext_unregister_dma(io_connect_t connection,
+                                            uint64_t iova);
+
+/*
+ * Read 8 bytes from a registered DMA region's IOMemoryDescriptor.
+ * Used to verify the descriptor references the same physical pages
+ * as the client's virtual mapping.
+ *
+ * @iova:     base IOVA of the registered region
+ * @offset:   byte offset within the region to read from
+ * @out_word: receives the 8-byte value read from the descriptor
+ */
+kern_return_t apple_dext_probe_dma(io_connect_t connection,
+                                       uint64_t iova,
+                                       uint64_t offset,
+                                       uint64_t *out_word);
+
+/*
+ * Read from PCI configuration space.
+ *
+ * @offset: byte offset into config space
+ * @width:  access width in bytes (1, 2, or 4)
+ * @out_value: receives the value read
+ */
+kern_return_t apple_dext_config_read(io_connect_t connection,
+                                         uint64_t offset,
+                                         uint64_t width,
+                                         uint64_t *out_value);
+
+/*
+ * Write to PCI configuration space.
+ *
+ * @offset: byte offset into config space
+ * @width:  access width in bytes (1, 2, or 4)
+ * @value:  value to write
+ */
+kern_return_t apple_dext_config_write(io_connect_t connection,
+                                          uint64_t offset,
+                                          uint64_t width,
+                                          uint64_t value);
+
+/*
+ * Read a contiguous block of PCI configuration space.
+ * Internally issues repeated 32-bit reads, with a final
+ * narrower read for any trailing bytes.
+ *
+ * @offset: starting byte offset
+ * @buf:    destination buffer
+ * @len:    number of bytes to read
+ */
+kern_return_t apple_dext_config_read_block(io_connect_t connection,
+                                               uint64_t offset,
+                                               void *buf,
+                                               size_t len);
+
+/*
+ * Query BAR information from the PCI device.
+ *
+ * @bar:          BAR index (0-5)
+ * @out_mem_idx:  receives the memory index for MemoryRead/Write calls
+ * @out_size:     receives the BAR size in bytes
+ * @out_type:     receives the BAR type (mem32, mem64, io, etc.)
+ */
+kern_return_t apple_dext_get_bar_info(io_connect_t connection,
+                                          uint8_t bar,
+                                          uint8_t *out_mem_idx,
+                                          uint64_t *out_size,
+                                          uint8_t *out_type);
+
+/*
+ * Map a PCI BAR directly into this process through the dext.
+ *
+ * The dext supplies the BAR's IOMemoryDescriptor and IOKit applies the
+ * appropriate cache mode for the BAR type (default-cache for BAR0 style
+ * register windows, write-combine for prefetchable apertures).
+ *
+ * @bar:      BAR index (0-5)
+ * @out_addr: receives the mapped virtual address
+ * @out_size: receives the mapped size
+ * @out_type: receives the BAR type (may be NULL)
+ */
+kern_return_t apple_dext_map_bar(io_connect_t connection,
+                                     uint8_t bar,
+                                     mach_vm_address_t *out_addr,
+                                     mach_vm_size_t *out_size,
+                                     uint8_t *out_type);
+
+/*
+ * Unmap a BAR previously mapped with apple_dext_map_bar().
+ */
+kern_return_t apple_dext_unmap_bar(io_connect_t connection,
+                                       uint8_t bar,
+                                       mach_vm_address_t addr);
+
+/*
+ * Read from a PCI BAR (MMIO).
+ *
+ * @mem_idx: memory index from apple_dext_get_bar_info
+ * @offset:  byte offset within the BAR
+ * @width:   access width in bytes (1, 2, 4, or 8)
+ * @out_value: receives the value read
+ */
+kern_return_t apple_dext_mmio_read(io_connect_t connection,
+                                       uint8_t mem_idx,
+                                       uint64_t offset,
+                                       uint64_t width,
+                                       uint64_t *out_value);
+
+/*
+ * Write to a PCI BAR (MMIO).
+ *
+ * @mem_idx: memory index from apple_dext_get_bar_info
+ * @offset:  byte offset within the BAR
+ * @width:   access width in bytes (1, 2, 4, or 8)
+ * @value:   value to write
+ */
+kern_return_t apple_dext_mmio_write(io_connect_t connection,
+                                        uint8_t mem_idx,
+                                        uint64_t offset,
+                                        uint64_t width,
+                                        uint64_t value);
+
+/*
+ * Set up interrupt forwarding for the PCI device.
+ * Creates IOInterruptDispatchSource handlers for all available
+ * MSI/MSI-X vectors in the dext. Interrupts are queued in a ring
+ * buffer and retrieved via apple_dext_check_interrupt().
+ *
+ * @out_num_vectors: receives the number of interrupt vectors registered
+ */
+kern_return_t apple_dext_setup_interrupts(io_connect_t connection,
+                                              uint32_t *out_num_vectors);
+
+/*
+ * Reset the PCI device via the dext.  Tries FLR first, then falls
+ * back to PM reset (D3hot → D0 transition).
+ */
+kern_return_t apple_dext_reset_device(io_connect_t connection);
+
+/*
+ * Set the IRQ enable mask in the dext.  Only vectors with their
+ * corresponding bit set will be recorded as pending when the
+ * hardware fires.  mask[] is 4 x uint64_t covering 256 vectors.
+ */
+kern_return_t apple_dext_set_irq_mask(io_connect_t connection,
+                                      const uint64_t mask[4]);
+
+/*
+ * Read and clear all pending interrupt bits from the dext.
+ * Returns up to 256 bits (4 MSI/MSI-X vectors per bit) across
+ * 4 uint64_t words.  Each bit that was set is atomically cleared
+ * in the dext.
+ */
+kern_return_t apple_dext_read_pending_irqs(io_connect_t connection,
+                                           uint64_t pending[4]);
+
+/*
+ * Opaque state for async interrupt notification from the dext.
+ */
+typedef struct AppleDextInterruptNotify AppleDextInterruptNotify;
+
+/*
+ * Create async interrupt notification.  handler_fn is called on a GCD
+ * dispatch queue whenever the dext signals that one or more interrupt
+ * bits have been set.  The handler should wake the QEMU main loop,
+ * which then calls apple_dext_read_pending_irqs() to drain the bits.
+ *
+ * The notification is armed immediately upon creation.
+ */
+AppleDextInterruptNotify *
+apple_dext_interrupt_notify_create(io_connect_t connection,
+                                   void (*handler_fn)(void *opaque),
+                                   void *opaque);
+
+/*
+ * Re-arm the async interrupt notification after draining pending bits.
+ * Must be called after each wakeup to receive subsequent notifications.
+ */
+kern_return_t
+apple_dext_interrupt_notify_rearm(AppleDextInterruptNotify *notify);
+
+/*
+ * Tear down and free async interrupt notification state.
+ */
+void apple_dext_interrupt_notify_destroy(AppleDextInterruptNotify *notify);
+
+#endif /* HW_VFIO_APPLE_DEXT_CLIENT_H */
diff --git a/hw/vfio/meson.build b/hw/vfio/meson.build
index 1ee9c11d5b..965c8e5b80 100644
--- a/hw/vfio/meson.build
+++ b/hw/vfio/meson.build
@@ -36,3 +36,10 @@ system_ss.add(when: 'CONFIG_IOMMUFD', if_false: files('iommufd-stubs.c'))
 system_ss.add(when: 'CONFIG_VFIO_PCI', if_true: files(
   'display.c',
 ))
+
+# Apple VFIO backend
+if host_os == 'darwin'
+  system_ss.add(when: 'CONFIG_VFIO',
+                if_true: [files('apple-dext-client.c'),
+                coref, iokit])
+endif
-- 
2.50.1 (Apple Git-155)