{"id":2219878,"url":"http://patchwork.ozlabs.org/api/1.2/patches/2219878/?format=json","web_url":"http://patchwork.ozlabs.org/project/qemu-devel/patch/20260405072857.66484-10-scottjgo@gmail.com/","project":{"id":14,"url":"http://patchwork.ozlabs.org/api/1.2/projects/14/?format=json","name":"QEMU Development","link_name":"qemu-devel","list_id":"qemu-devel.nongnu.org","list_email":"qemu-devel@nongnu.org","web_url":"","scm_url":"","webscm_url":"","list_archive_url":"","list_archive_url_format":"","commit_url_format":""},"msgid":"<20260405072857.66484-10-scottjgo@gmail.com>","list_archive_url":null,"date":"2026-04-05T07:28:53","name":"[RFC,09/10] vfio/apple: Add apple-dma-pci companion device","commit_ref":null,"pull_url":null,"state":"new","archived":false,"hash":"e07a950cab2537fab085835c1bc92edcbf3c7607","submitter":{"id":93060,"url":"http://patchwork.ozlabs.org/api/1.2/people/93060/?format=json","name":"Scott J. Goldman","email":"scottjgo@gmail.com"},"delegate":null,"mbox":"http://patchwork.ozlabs.org/project/qemu-devel/patch/20260405072857.66484-10-scottjgo@gmail.com/mbox/","series":[{"id":498765,"url":"http://patchwork.ozlabs.org/api/1.2/series/498765/?format=json","web_url":"http://patchwork.ozlabs.org/project/qemu-devel/list/?series=498765","date":"2026-04-05T07:28:44","name":"vfio: PCI device passthrough on Apple Silicon Macs","version":1,"mbox":"http://patchwork.ozlabs.org/series/498765/mbox/"}],"comments":"http://patchwork.ozlabs.org/api/patches/2219878/comments/","check":"pending","checks":"http://patchwork.ozlabs.org/api/patches/2219878/checks/","tags":{},"related":[],"headers":{"Return-Path":"<qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org>","X-Original-To":"incoming@patchwork.ozlabs.org","Delivered-To":"patchwork-incoming@legolas.ozlabs.org","Authentication-Results":["legolas.ozlabs.org;\n\tdkim=pass (2048-bit key;\n unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256\n header.s=20251104 header.b=hd64ncbP;\n\tdkim-atps=neutral","legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=nongnu.org\n (client-ip=209.51.188.17; helo=lists.gnu.org;\n envelope-from=qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org;\n receiver=patchwork.ozlabs.org)"],"Received":["from lists.gnu.org (lists.gnu.org [209.51.188.17])\n\t(using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fpPHg2Kt8z1yFs\n\tfor <incoming@patchwork.ozlabs.org>; Sun, 05 Apr 2026 17:30:06 +1000 (AEST)","from localhost ([::1] helo=lists1p.gnu.org)\n\tby lists.gnu.org with esmtp (Exim 4.90_1)\n\t(envelope-from <qemu-devel-bounces@nongnu.org>)\n\tid 1w9HvU-0003yr-M4; Sun, 05 Apr 2026 03:29:44 -0400","from eggs.gnu.org ([2001:470:142:3::10])\n by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <scottjgo@gmail.com>)\n id 1w9HvT-0003yC-82\n for qemu-devel@nongnu.org; Sun, 05 Apr 2026 03:29:43 -0400","from mail-dy1-x1329.google.com ([2607:f8b0:4864:20::1329])\n by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128)\n (Exim 4.90_1) (envelope-from <scottjgo@gmail.com>)\n id 1w9HvQ-0007Px-Oa\n for qemu-devel@nongnu.org; Sun, 05 Apr 2026 03:29:43 -0400","by mail-dy1-x1329.google.com with SMTP id\n 5a478bee46e88-2cf1646bd11so348020eec.1\n for <qemu-devel@nongnu.org>; Sun, 05 Apr 2026 00:29:40 -0700 (PDT)","from localhost.localdomain ([2601:645:8200:47:41e4:ff2b:ff70:4d75])\n by smtp.gmail.com with ESMTPSA id\n 5a478bee46e88-2cb92ea0ef1sm7636502eec.21.2026.04.05.00.29.37\n (version=TLS1_3 cipher=TLS_CHACHA20_POLY1305_SHA256 bits=256/256);\n Sun, 05 Apr 2026 00:29:38 -0700 (PDT)"],"DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=gmail.com; s=20251104; t=1775374179; x=1775978979; darn=nongnu.org;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:from:to:cc:subject:date\n :message-id:reply-to;\n bh=fWuxDaZI7ELjXX3VdrMom5tpL3p2EOAcG4ltkUwfmlI=;\n b=hd64ncbPiGbELjHIkCPLOZwPWamN5YQ8kOjRpS4u28JeWHN3980jmI4qFLAPdUWmY1\n aUZCbTZfpg4GRDhVBN5iL0FkWe0BN37ZeBcK6qQU7N1DgFG3dpYgT+ZUGv6EqZ9ZFQgt\n hALLTT4OHaVrHx3N5hlnI2wOa6bPOc/jkBR1VrM50E7ltw+Dq1sE5MhFisLL2m/dbfOw\n 8pT9+Y+GT4q6zQt9GJgRjcrm1Xmv3513A/aV2vqDZD+eNcYIqmyBKeo7bNkEKF88o0Jp\n MFKA3xL74ZuuZqnAfjZAxwPhI0i8XEBBglyylt+JFcWiYdB4vs9a6m7W23ulEWw9NJwl\n SoBw==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n d=1e100.net; s=20251104; t=1775374179; x=1775978979;\n h=content-transfer-encoding:mime-version:references:in-reply-to\n :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from\n :to:cc:subject:date:message-id:reply-to;\n bh=fWuxDaZI7ELjXX3VdrMom5tpL3p2EOAcG4ltkUwfmlI=;\n b=YNlJKxAP13cX4kr5Uo15Zh5b63DhEamiahUpXXs3ZVZWiJIBESYRFN9z/vvvfWUzfQ\n 2WzNWAHyvazhtHOMAA6GcRkuZN1l2ZNUicVuFz42iznoYug6QZ/cLUkX2P1+2JQPjrf5\n JqzwwpG8k+bPsAQ2NeQvgsgr7UztksTxajdE9MEVTB/WW41VVWMxKl7IibUcvvjIXygq\n kT/OyaxeWbSgGXtOFyNqgza0fXFKFYj2Zmtrwdw98zbBq2YRxZ5hCNbHX6JZv/xZ6iz0\n cj/s0jvkaXTTLzAfeQwc0rH684j4L73Pmwx64j8nrgYH8lLVLmpfEExP7OmRD8KtiAWx\n 1KpA==","X-Gm-Message-State":"AOJu0Yx1cO8hLrJCQ+FLeE/vxmCRHwszLw66mkDsN6YsWFlkobVDufV3\n A5Dw2fc2qcV4bWX+ilDUDbgaTrGBw3IUGg/uwF08TRyKtK2o6IZ+SanHpetPVhzRg0I=","X-Gm-Gg":"AeBDiet28+eErUrAKMSqPRC2eOfdbop4oWdkrkKKNhF12s2QRf3UHFraXBIHvhowHr4\n zigyqfFcrvPcLfHCQNiXgTZM7QmT37symahYpfiTDDM+D7K0rzIDgNXbI6zJRc3xV5i5S1goEXs\n M+H8WUdPYw0go1UDpgcEdlUlrT1EXbuAsHCZtzJJPaCyQ2IlItwJhkGXmX3KDgGqDopl8hhSa43\n 92Mcvm5+xq6T+tOlfIv6+F3ElB3Nc7DfKtn8qOS8PkRvTEJLr883hrdRp+qJ1J1jTQFYjffbbvw\n 03mkt8cmqyYYSgTyL7bHXQthCopkoffS9Ukwb9kZ7N8jlGOa9CG7c+uaShP2sQXP6iXXDe2/+Gq\n 52c/1IZ4e0M9LcTgLfvSlB6NKa90zyT6WM4/jkZTG5TSBKymT1XuBMa51m9Lh7KSOGDBdC2AR42\n QZMACg1T34sTX3lZJaDnsV5WzZYDaFM05IlE+7arDvt29rxA/kWaTQq8WV87XUJGj2XM1drwiiH\n NHcfQh8fdFKqHnI1UoIUJjt/3Q=","X-Received":"by 2002:a05:693c:3018:b0:2c0:cc90:a71 with SMTP id\n 5a478bee46e88-2cbf9df22d4mr3755706eec.8.1775374178818;\n Sun, 05 Apr 2026 00:29:38 -0700 (PDT)","From":"\"Scott J. Goldman\" <scottjgo@gmail.com>","To":"qemu-devel@nongnu.org","Cc":"alex@shazbot.org, clg@redhat.com, pbonzini@redhat.com, rbolshakov@ddn.com,\n phil@philjordan.eu, mst@redhat.com, john.levon@nutanix.com,\n thanos.makatos@nutanix.com, qemu-s390x@nongnu.org,\n \"Scott J. Goldman\" <scottjg@umich.edu>,\n \"Scott J. Goldman\" <scottjgo@gmail.com>","Subject":"[RFC PATCH 09/10] vfio/apple: Add apple-dma-pci companion device","Date":"Sun,  5 Apr 2026 00:28:53 -0700","Message-ID":"<20260405072857.66484-10-scottjgo@gmail.com>","X-Mailer":"git-send-email 2.50.1","In-Reply-To":"<20260405072857.66484-1-scottjgo@gmail.com>","References":"<20260405072857.66484-1-scottjgo@gmail.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Received-SPF":"pass client-ip=2607:f8b0:4864:20::1329;\n envelope-from=scottjgo@gmail.com; helo=mail-dy1-x1329.google.com","X-Spam_score_int":"-20","X-Spam_score":"-2.1","X-Spam_bar":"--","X-Spam_report":"(-2.1 / 5.0 requ) BAYES_00=-1.9, DKIM_SIGNED=0.1,\n DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, FREEMAIL_FROM=0.001,\n RCVD_IN_DNSWL_NONE=-0.0001, SPF_HELO_NONE=0.001,\n SPF_PASS=-0.001 autolearn=unavailable autolearn_force=no","X-Spam_action":"no action","X-BeenThere":"qemu-devel@nongnu.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"qemu development <qemu-devel.nongnu.org>","List-Unsubscribe":"<https://lists.nongnu.org/mailman/options/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=unsubscribe>","List-Archive":"<https://lists.nongnu.org/archive/html/qemu-devel>","List-Post":"<mailto:qemu-devel@nongnu.org>","List-Help":"<mailto:qemu-devel-request@nongnu.org?subject=help>","List-Subscribe":"<https://lists.nongnu.org/mailman/listinfo/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=subscribe>","Errors-To":"qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org","Sender":"qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org"},"content":"From: \"Scott J. Goldman\" <scottjg@umich.edu>\n\nAdd a virtual PCI device (\"apple-dma-pci\") that acts as the DMA\nmapping companion for passthrough devices on macOS.\n\nOn Linux, the kernel VFIO driver can directly map guest RAM into the\nhardware IOMMU.  On macOS, the DriverKit dext can only DMA-map memory\nthat belongs to its client process (QEMU).  The guest kernel needs a\nway to request DMA mappings for its buffers through the host.\n\napple-dma-pci provides a simple MMIO register interface in the guest:\nthe guest driver writes DMA mapping requests (IOVA, GPA, size) to the\ndevice's BAR, and the QEMU device model translates GPAs to HVAs and\ncalls through to the dext to establish the physical IOMMU mapping.\n\nThe device uses PCI vendor/device ID 1b36:0015 (Red Hat, Inc.).\n\nSigned-off-by: Scott J. Goldman <scottjgo@gmail.com>\n---\n docs/specs/pci-ids.rst |   3 +\n hw/vfio/apple-dma.c    | 540 +++++++++++++++++++++++++++++++++++++++++\n hw/vfio/meson.build    |   5 +-\n include/hw/pci/pci.h   |   1 +\n 4 files changed, 546 insertions(+), 3 deletions(-)\n create mode 100644 hw/vfio/apple-dma.c","diff":"diff --git a/docs/specs/pci-ids.rst b/docs/specs/pci-ids.rst\nindex 261b0f359f..48229dab5d 100644\n--- a/docs/specs/pci-ids.rst\n+++ b/docs/specs/pci-ids.rst\n@@ -100,6 +100,9 @@ PCI devices (other than virtio):\n   PCI UFS device (``-device ufs``)\n 1b36:0014\n   PCI RISC-V IOMMU device\n+1b36:0015\n+  Apple DMA mapping device (``-device apple-dma-pci``,\n+  :doc:`../system/devices/vfio-apple`)\n \n All these devices are documented in :doc:`index`.\n \ndiff --git a/hw/vfio/apple-dma.c b/hw/vfio/apple-dma.c\nnew file mode 100644\nindex 0000000000..e705179b0d\n--- /dev/null\n+++ b/hw/vfio/apple-dma.c\n@@ -0,0 +1,540 @@\n+/*\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ *\n+ * Apple DMA mapping PCI device\n+ *\n+ * A simple PCI device that receives batched DMA map/unmap requests from\n+ * the guest via a shared command page + doorbell register, resolves guest\n+ * physical addresses to host virtual addresses, and registers them with\n+ * the macOS DriverKit dext for DART mapping.\n+ *\n+ * Protocol:\n+ *   1. Guest allocates a command page and request/response buffers in RAM.\n+ *   2. Guest writes the command page GPA to BAR registers (one-time setup).\n+ *   3. Per batch: guest fills the command page and request buffer (no VMEXIT),\n+ *      then writes the doorbell register (single VMEXIT triggers processing).\n+ *   4. Device reads the command page, processes all entries, writes responses\n+ *      and status back to guest RAM before the doorbell write returns.\n+ *\n+ * Copyright (c) 2026 Scott J. Goldman\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"hw/pci/pci_device.h\"\n+#include \"hw/core/qdev-properties.h\"\n+#include \"hw/vfio/apple-dext-client.h\"\n+#include \"hw/vfio/apple.h\"\n+#include \"qapi/error.h\"\n+#include \"qemu/error-report.h\"\n+#include \"qemu/module.h\"\n+#include \"system/address-spaces.h\"\n+#include \"system/dma.h\"\n+#include \"system/memory.h\"\n+\n+#include \"hw/pci/pci.h\"\n+\n+/* BAR0 register offsets */\n+#define APPLE_DMA_REG_VERSION       0x00    /* R:  protocol version */\n+#define APPLE_DMA_REG_MANAGED_BDF   0x04    /* R:  guest BDF this maps for */\n+#define APPLE_DMA_REG_MAX_ENTRIES   0x08    /* R:  max entries per batch */\n+#define APPLE_DMA_REG_STATUS        0x0C    /* R:  result of last doorbell */\n+#define APPLE_DMA_REG_CMD_GPA_LO    0x10    /* W:  command page GPA [31:0] */\n+#define APPLE_DMA_REG_CMD_GPA_HI    0x14    /* W:  command page GPA [63:32] */\n+#define APPLE_DMA_REG_DOORBELL      0x18    /* W:  any write triggers batch */\n+#define APPLE_DMA_BAR_SIZE          0x1000  /* page-aligned */\n+\n+#define APPLE_DMA_VERSION           2\n+#define APPLE_DMA_MAX_ENTRIES       4096\n+\n+/* Command types (in command page) */\n+#define APPLE_DMA_CMD_MAP           1\n+#define APPLE_DMA_CMD_UNMAP         2\n+\n+/* Status codes */\n+#define APPLE_DMA_S_OK              0\n+#define APPLE_DMA_S_IOERR           1\n+#define APPLE_DMA_S_INVAL           3\n+\n+/*\n+ * Command page layout (in guest RAM, 32 bytes):\n+ *\n+ *   0x00  uint32_t  type         MAP=1, UNMAP=2\n+ *   0x04  uint32_t  count        number of entries\n+ *   0x08  uint32_t  status       (written by device) 0=OK\n+ *   0x0C  uint32_t  reserved\n+ *   0x10  uint64_t  req_gpa      GPA of request entries array\n+ *   0x18  uint64_t  resp_gpa     GPA of response entries array\n+ */\n+#define CMD_OFF_TYPE      0x00\n+#define CMD_OFF_COUNT     0x04\n+#define CMD_OFF_STATUS    0x08\n+#define CMD_OFF_REQ_GPA   0x10\n+#define CMD_OFF_RESP_GPA  0x18\n+#define CMD_PAGE_SIZE     0x20\n+\n+/*\n+ * Map request entry (16 bytes):\n+ *   uint64_t gpa, uint32_t len, uint32_t flags\n+ *\n+ * Map response entry (24 bytes):\n+ *   uint64_t id, uint64_t dma_addr, uint32_t dma_len, uint32_t status\n+ *\n+ * Unmap request entry (16 bytes):\n+ *   uint64_t id, uint64_t size\n+ *\n+ * Unmap response entry (16 bytes):\n+ *   uint64_t id, uint32_t status, uint32_t reserved\n+ */\n+\n+typedef struct AppleDMAMapReq {\n+    uint64_t gpa;\n+    uint32_t len;\n+    uint32_t flags;\n+} QEMU_PACKED AppleDMAMapReq;\n+\n+typedef struct AppleDMAMapResp {\n+    uint64_t id;\n+    uint64_t dma_addr;\n+    uint32_t dma_len;\n+    uint32_t status;\n+} QEMU_PACKED AppleDMAMapResp;\n+\n+typedef struct AppleDMAUnmapReq {\n+    uint64_t id;\n+    uint64_t size;\n+} QEMU_PACKED AppleDMAUnmapReq;\n+\n+typedef struct AppleDMAUnmapResp {\n+    uint64_t id;\n+    uint32_t status;\n+    uint32_t reserved;\n+} QEMU_PACKED AppleDMAUnmapResp;\n+\n+#define TYPE_APPLE_DMA_PCI \"apple-dma-pci\"\n+OBJECT_DECLARE_SIMPLE_TYPE(AppleDMAState, APPLE_DMA_PCI)\n+\n+struct AppleDMAState {\n+    PCIDevice parent_obj;\n+\n+    MemoryRegion bar;\n+\n+    /* Configuration (set via properties) */\n+    uint32_t managed_bdf;\n+    uint32_t max_entries;\n+    uint32_t apple_host_bus;\n+    uint32_t apple_host_device;\n+    uint32_t apple_host_function;\n+\n+    /* Runtime state */\n+    uint64_t cmd_gpa;\n+    uint32_t last_status;\n+    io_connect_t dext_conn;\n+    bool shared_dext_conn;\n+};\n+\n+/* ------------------------------------------------------------------ */\n+/* DMA backend operations                                              */\n+/* ------------------------------------------------------------------ */\n+\n+static bool apple_dma_backend_map(AppleDMAState *s, uint64_t gpa, uint32_t size,\n+                                  uint64_t *out_dma_addr, uint32_t *out_dma_len)\n+{\n+    hwaddr map_len = size;\n+    void *hva;\n+    uint64_t bus_addr = 0, bus_len = 0;\n+\n+    hva = dma_memory_map(&address_space_memory, gpa, &map_len,\n+                         DMA_DIRECTION_TO_DEVICE, MEMTXATTRS_UNSPECIFIED);\n+    if (!hva || map_len < size) {\n+        if (hva) {\n+            dma_memory_unmap(&address_space_memory, hva, map_len,\n+                             DMA_DIRECTION_TO_DEVICE, 0);\n+        }\n+        return false;\n+    }\n+\n+    /*\n+     * Use the GPA as the dext lookup key. The dext treats this as an\n+     * opaque handle for matching register/unregister calls; the actual\n+     * DMA bus address is assigned by the platform and returned in\n+     * bus_addr.\n+     */\n+    if (s->dext_conn != IO_OBJECT_NULL) {\n+        kern_return_t kr;\n+\n+        kr = apple_dext_register_dma(s->dext_conn, gpa,\n+                                     (uint64_t)hva, size,\n+                                     &bus_addr, &bus_len);\n+        dma_memory_unmap(&address_space_memory, hva, map_len,\n+                         DMA_DIRECTION_TO_DEVICE, 0);\n+        if (kr != KERN_SUCCESS) {\n+            return false;\n+        }\n+        *out_dma_addr = bus_addr;\n+        *out_dma_len = (uint32_t)bus_len;\n+    } else {\n+        dma_memory_unmap(&address_space_memory, hva, map_len,\n+                         DMA_DIRECTION_TO_DEVICE, 0);\n+        *out_dma_addr = gpa;\n+        *out_dma_len = size;\n+    }\n+\n+    return true;\n+}\n+\n+static uint32_t apple_dma_backend_unmap(AppleDMAState *s, uint64_t id)\n+{\n+    if (s->dext_conn != IO_OBJECT_NULL) {\n+        kern_return_t kr;\n+\n+        kr = apple_dext_unregister_dma(s->dext_conn, id);\n+        if (kr != KERN_SUCCESS) {\n+            return APPLE_DMA_S_IOERR;\n+        }\n+    }\n+\n+    return APPLE_DMA_S_OK;\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* Doorbell — process a batch from the command page                    */\n+/* ------------------------------------------------------------------ */\n+\n+static void apple_dma_handle_map(AppleDMAState *s, uint64_t req_gpa,\n+                                 uint64_t resp_gpa, uint32_t count)\n+{\n+    AddressSpace *as = &address_space_memory;\n+    hwaddr req_len = count * sizeof(AppleDMAMapReq);\n+    hwaddr resp_len = count * sizeof(AppleDMAMapResp);\n+    AppleDMAMapReq *reqs;\n+    AppleDMAMapResp *resps;\n+    uint32_t i;\n+    bool ok = true;\n+\n+    reqs = dma_memory_map(as, req_gpa, &req_len, DMA_DIRECTION_TO_DEVICE,\n+                          MEMTXATTRS_UNSPECIFIED);\n+    if (!reqs || req_len < count * sizeof(AppleDMAMapReq)) {\n+        if (reqs) {\n+            dma_memory_unmap(as, reqs, req_len, DMA_DIRECTION_TO_DEVICE, 0);\n+        }\n+        s->last_status = APPLE_DMA_S_INVAL;\n+        return;\n+    }\n+\n+    resps = dma_memory_map(as, resp_gpa, &resp_len, DMA_DIRECTION_FROM_DEVICE,\n+                           MEMTXATTRS_UNSPECIFIED);\n+    if (!resps || resp_len < count * sizeof(AppleDMAMapResp)) {\n+        if (resps) {\n+            dma_memory_unmap(as, resps, resp_len,\n+                             DMA_DIRECTION_FROM_DEVICE, 0);\n+        }\n+        dma_memory_unmap(as, reqs, req_len, DMA_DIRECTION_TO_DEVICE, 0);\n+        s->last_status = APPLE_DMA_S_INVAL;\n+        return;\n+    }\n+\n+    for (i = 0; i < count; i++) {\n+        uint64_t gpa = le64_to_cpu(reqs[i].gpa);\n+        uint64_t dma_addr = 0;\n+        uint32_t dma_len = 0;\n+\n+        if (apple_dma_backend_map(s, gpa, le32_to_cpu(reqs[i].len),\n+                                  &dma_addr, &dma_len)) {\n+            resps[i].id = cpu_to_le64(gpa);\n+            resps[i].dma_addr = cpu_to_le64(dma_addr);\n+            resps[i].dma_len = cpu_to_le32(dma_len);\n+            resps[i].status = cpu_to_le32(APPLE_DMA_S_OK);\n+        } else {\n+            resps[i].status = cpu_to_le32(APPLE_DMA_S_IOERR);\n+            ok = false;\n+        }\n+    }\n+\n+    s->last_status = ok ? APPLE_DMA_S_OK : APPLE_DMA_S_IOERR;\n+    dma_memory_unmap(as, resps, resp_len, DMA_DIRECTION_FROM_DEVICE,\n+                     resp_len);\n+    dma_memory_unmap(as, reqs, req_len, DMA_DIRECTION_TO_DEVICE, 0);\n+}\n+\n+static void apple_dma_handle_unmap(AppleDMAState *s, uint64_t req_gpa,\n+                                   uint64_t resp_gpa, uint32_t count)\n+{\n+    AddressSpace *as = &address_space_memory;\n+    hwaddr req_len = count * sizeof(AppleDMAUnmapReq);\n+    hwaddr resp_len = count * sizeof(AppleDMAUnmapResp);\n+    AppleDMAUnmapReq *reqs;\n+    AppleDMAUnmapResp *resps;\n+    uint32_t i;\n+    bool ok = true;\n+\n+    reqs = dma_memory_map(as, req_gpa, &req_len, DMA_DIRECTION_TO_DEVICE,\n+                          MEMTXATTRS_UNSPECIFIED);\n+    if (!reqs || req_len < count * sizeof(AppleDMAUnmapReq)) {\n+        if (reqs) {\n+            dma_memory_unmap(as, reqs, req_len, DMA_DIRECTION_TO_DEVICE, 0);\n+        }\n+        s->last_status = APPLE_DMA_S_INVAL;\n+        return;\n+    }\n+\n+    resps = dma_memory_map(as, resp_gpa, &resp_len, DMA_DIRECTION_FROM_DEVICE,\n+                           MEMTXATTRS_UNSPECIFIED);\n+    if (!resps || resp_len < count * sizeof(AppleDMAUnmapResp)) {\n+        if (resps) {\n+            dma_memory_unmap(as, resps, resp_len,\n+                             DMA_DIRECTION_FROM_DEVICE, 0);\n+        }\n+        dma_memory_unmap(as, reqs, req_len, DMA_DIRECTION_TO_DEVICE, 0);\n+        s->last_status = APPLE_DMA_S_INVAL;\n+        return;\n+    }\n+\n+    for (i = 0; i < count; i++) {\n+        uint64_t id = le64_to_cpu(reqs[i].id);\n+        uint32_t status = apple_dma_backend_unmap(s, id);\n+\n+        resps[i].id = cpu_to_le64(id);\n+        resps[i].status = cpu_to_le32(status);\n+        if (status != APPLE_DMA_S_OK) {\n+            ok = false;\n+        }\n+    }\n+\n+    s->last_status = ok ? APPLE_DMA_S_OK : APPLE_DMA_S_IOERR;\n+    dma_memory_unmap(as, resps, resp_len, DMA_DIRECTION_FROM_DEVICE,\n+                     resp_len);\n+    dma_memory_unmap(as, reqs, req_len, DMA_DIRECTION_TO_DEVICE, 0);\n+}\n+\n+static void apple_dma_doorbell(AppleDMAState *s)\n+{\n+    AddressSpace *as = &address_space_memory;\n+    uint8_t cmd_buf[CMD_PAGE_SIZE];\n+    uint32_t type, count;\n+    uint64_t req_gpa, resp_gpa;\n+    uint32_t le_status;\n+\n+    if (!s->cmd_gpa) {\n+        s->last_status = APPLE_DMA_S_INVAL;\n+        return;\n+    }\n+\n+    if (dma_memory_read(as, s->cmd_gpa, cmd_buf, CMD_PAGE_SIZE,\n+                        MEMTXATTRS_UNSPECIFIED) != MEMTX_OK) {\n+        s->last_status = APPLE_DMA_S_INVAL;\n+        return;\n+    }\n+\n+    type = ldl_le_p(cmd_buf + CMD_OFF_TYPE);\n+    count = ldl_le_p(cmd_buf + CMD_OFF_COUNT);\n+    req_gpa = ldq_le_p(cmd_buf + CMD_OFF_REQ_GPA);\n+    resp_gpa = ldq_le_p(cmd_buf + CMD_OFF_RESP_GPA);\n+\n+    if (!count || count > s->max_entries || !req_gpa || !resp_gpa) {\n+        s->last_status = APPLE_DMA_S_INVAL;\n+        goto write_status;\n+    }\n+\n+    switch (type) {\n+    case APPLE_DMA_CMD_MAP:\n+        apple_dma_handle_map(s, req_gpa, resp_gpa, count);\n+        break;\n+    case APPLE_DMA_CMD_UNMAP:\n+        apple_dma_handle_unmap(s, req_gpa, resp_gpa, count);\n+        break;\n+    default:\n+        s->last_status = APPLE_DMA_S_INVAL;\n+        break;\n+    }\n+\n+write_status:\n+    le_status = cpu_to_le32(s->last_status);\n+    dma_memory_write(as, s->cmd_gpa + CMD_OFF_STATUS, &le_status, 4,\n+                     MEMTXATTRS_UNSPECIFIED);\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* MMIO BAR handlers                                                   */\n+/* ------------------------------------------------------------------ */\n+\n+static uint64_t apple_dma_bar_read(void *opaque, hwaddr addr, unsigned size)\n+{\n+    AppleDMAState *s = opaque;\n+\n+    switch (addr) {\n+    case APPLE_DMA_REG_VERSION:\n+        return APPLE_DMA_VERSION;\n+    case APPLE_DMA_REG_MANAGED_BDF:\n+        return s->managed_bdf;\n+    case APPLE_DMA_REG_MAX_ENTRIES:\n+        return s->max_entries;\n+    case APPLE_DMA_REG_STATUS:\n+        return s->last_status;\n+    default:\n+        return 0;\n+    }\n+}\n+\n+static void apple_dma_bar_write(void *opaque, hwaddr addr, uint64_t val,\n+                                unsigned size)\n+{\n+    AppleDMAState *s = opaque;\n+\n+    switch (addr) {\n+    case APPLE_DMA_REG_CMD_GPA_LO:\n+        s->cmd_gpa = deposit64(s->cmd_gpa, 0, 32, val);\n+        break;\n+    case APPLE_DMA_REG_CMD_GPA_HI:\n+        s->cmd_gpa = deposit64(s->cmd_gpa, 32, 32, val);\n+        break;\n+    case APPLE_DMA_REG_DOORBELL:\n+        apple_dma_doorbell(s);\n+        break;\n+    default:\n+        break;\n+    }\n+}\n+\n+static const MemoryRegionOps apple_dma_bar_ops = {\n+    .read = apple_dma_bar_read,\n+    .write = apple_dma_bar_write,\n+    .endianness = DEVICE_LITTLE_ENDIAN,\n+    .impl = {\n+        .min_access_size = 4,\n+        .max_access_size = 4,\n+    },\n+};\n+\n+/* ------------------------------------------------------------------ */\n+/* Dext connection                                                     */\n+/* ------------------------------------------------------------------ */\n+\n+static bool apple_dma_connect_dext(AppleDMAState *s, Error **errp)\n+{\n+    io_connect_t conn;\n+    kern_return_t kr;\n+\n+    conn = apple_vfio_dext_lookup(s->apple_host_bus, s->apple_host_device,\n+                                  s->apple_host_function);\n+    if (conn != IO_OBJECT_NULL) {\n+        s->dext_conn = conn;\n+        s->shared_dext_conn = true;\n+        return true;\n+    }\n+\n+    conn = apple_dext_connect(s->apple_host_bus, s->apple_host_device,\n+                                  s->apple_host_function);\n+    if (conn == IO_OBJECT_NULL) {\n+        error_setg(errp,\n+                   \"apple-dma: could not connect to dext for host PCI \"\n+                   \"%02x:%02x.%x\",\n+                   s->apple_host_bus, s->apple_host_device,\n+                   s->apple_host_function);\n+        return false;\n+    }\n+\n+    kr = apple_dext_claim(conn);\n+    if (kr != KERN_SUCCESS) {\n+        error_setg(errp,\n+                   \"apple-dma: failed to claim dext-backed PCI device \"\n+                   \"%02x:%02x.%x (kr=0x%x)\",\n+                   s->apple_host_bus, s->apple_host_device,\n+                   s->apple_host_function, kr);\n+        apple_dext_disconnect(conn);\n+        return false;\n+    }\n+\n+    s->dext_conn = conn;\n+    s->shared_dext_conn = false;\n+    return true;\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* PCI device lifecycle                                                */\n+/* ------------------------------------------------------------------ */\n+\n+static void apple_dma_pci_realize(PCIDevice *pdev, Error **errp)\n+{\n+    AppleDMAState *s = APPLE_DMA_PCI(pdev);\n+\n+    if (s->apple_host_bus == UINT32_MAX ||\n+        s->apple_host_device == UINT32_MAX ||\n+        s->apple_host_function == UINT32_MAX) {\n+        error_setg(errp, \"apple-dma: requires x-apple-host-bus, \"\n+                   \"x-apple-host-device, and x-apple-host-function\");\n+        return;\n+    }\n+\n+    if (!s->max_entries) {\n+        s->max_entries = APPLE_DMA_MAX_ENTRIES;\n+    }\n+\n+    if (!apple_dma_connect_dext(s, errp)) {\n+        return;\n+    }\n+\n+    memory_region_init_io(&s->bar, OBJECT(s), &apple_dma_bar_ops, s,\n+                          \"apple-dma-bar\", APPLE_DMA_BAR_SIZE);\n+    pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->bar);\n+}\n+\n+static void apple_dma_pci_exit(PCIDevice *pdev)\n+{\n+    AppleDMAState *s = APPLE_DMA_PCI(pdev);\n+\n+    if (s->dext_conn != IO_OBJECT_NULL) {\n+        if (s->shared_dext_conn) {\n+            apple_vfio_dext_release(s->apple_host_bus, s->apple_host_device,\n+                                    s->apple_host_function, s->dext_conn);\n+        } else {\n+            apple_dext_disconnect(s->dext_conn);\n+        }\n+        s->dext_conn = IO_OBJECT_NULL;\n+    }\n+}\n+\n+static const Property apple_dma_pci_properties[] = {\n+    DEFINE_PROP_UINT32(\"managed-bdf\", AppleDMAState, managed_bdf, 0),\n+    DEFINE_PROP_UINT32(\"max-entries\", AppleDMAState, max_entries, 0),\n+    DEFINE_PROP_UINT32(\"x-apple-host-bus\", AppleDMAState,\n+                       apple_host_bus, UINT32_MAX),\n+    DEFINE_PROP_UINT32(\"x-apple-host-device\", AppleDMAState,\n+                       apple_host_device, UINT32_MAX),\n+    DEFINE_PROP_UINT32(\"x-apple-host-function\", AppleDMAState,\n+                       apple_host_function, UINT32_MAX),\n+};\n+\n+static void apple_dma_pci_class_init(ObjectClass *klass, const void *data)\n+{\n+    DeviceClass *dc = DEVICE_CLASS(klass);\n+    PCIDeviceClass *pdc = PCI_DEVICE_CLASS(klass);\n+\n+    pdc->realize = apple_dma_pci_realize;\n+    pdc->exit = apple_dma_pci_exit;\n+    pdc->vendor_id = PCI_VENDOR_ID_REDHAT;\n+    pdc->device_id = PCI_DEVICE_ID_REDHAT_APPLE_DMA;\n+    pdc->class_id = PCI_CLASS_SYSTEM_OTHER;\n+\n+    set_bit(DEVICE_CATEGORY_MISC, dc->categories);\n+    device_class_set_props(dc, apple_dma_pci_properties);\n+    dc->desc = \"Apple DMA mapping device\";\n+}\n+\n+static const TypeInfo apple_dma_pci_info = {\n+    .name = TYPE_APPLE_DMA_PCI,\n+    .parent = TYPE_PCI_DEVICE,\n+    .instance_size = sizeof(AppleDMAState),\n+    .class_init = apple_dma_pci_class_init,\n+    .interfaces = (const InterfaceInfo[]) {\n+        { INTERFACE_CONVENTIONAL_PCI_DEVICE },\n+        { },\n+    },\n+};\n+\n+static void apple_dma_pci_register(void)\n+{\n+    type_register_static(&apple_dma_pci_info);\n+}\n+\n+type_init(apple_dma_pci_register)\ndiff --git a/hw/vfio/meson.build b/hw/vfio/meson.build\nindex 473f8669f9..d7b4cbcc19 100644\n--- a/hw/vfio/meson.build\n+++ b/hw/vfio/meson.build\n@@ -40,8 +40,7 @@ system_ss.add(when: 'CONFIG_VFIO_PCI', if_true: files(\n # Apple VFIO backend\n if host_os == 'darwin'\n   system_ss.add(when: 'CONFIG_VFIO',\n-                if_true: [files('apple-device.c',\n-                                'container-apple.c',\n-                                'apple-dext-client.c'),\n+                if_true: [files('apple-device.c', 'apple-dma.c',\n+                                'container-apple.c', 'apple-dext-client.c'),\n                           coref, iokit])\n endif\ndiff --git a/include/hw/pci/pci.h b/include/hw/pci/pci.h\nindex 5b179091de..8e3fe77cc7 100644\n--- a/include/hw/pci/pci.h\n+++ b/include/hw/pci/pci.h\n@@ -121,6 +121,7 @@ extern bool pci_available;\n #define PCI_DEVICE_ID_REDHAT_ACPI_ERST   0x0012\n #define PCI_DEVICE_ID_REDHAT_UFS         0x0013\n #define PCI_DEVICE_ID_REDHAT_RISCV_IOMMU 0x0014\n+#define PCI_DEVICE_ID_REDHAT_APPLE_DMA   0x0015\n #define PCI_DEVICE_ID_REDHAT_QXL         0x0100\n \n #define FMT_PCIBUS                      PRIx64\n","prefixes":["RFC","09/10"]}