diff --git a/include/hw/remote/ioregionfd.h b/include/hw/remote/ioregionfd.h
index 66bb459f76..8021eed6f1 100644
--- a/include/hw/remote/ioregionfd.h
+++ b/include/hw/remote/ioregionfd.h
@@ -40,4 +40,6 @@ typedef struct IORegionFDObject IORegionFDObject;
 GSList *ioregionfd_get_obj_list(void);
 IORegionFD *ioregionfd_get_by_bar(GSList *list, uint32_t bar);
 void ioregionfd_set_bar_type(GSList *list, uint32_t bar, bool memory);
+int qio_channel_ioregionfd_read(QIOChannel *ioc, gpointer opaque,
+                                Error **errp);
 #endif /* IOREGIONFD_H */
diff --git a/include/hw/remote/remote.h b/include/hw/remote/remote.h
index 46390c7934..53b570e1ac 100644
--- a/include/hw/remote/remote.h
+++ b/include/hw/remote/remote.h
@@ -23,6 +23,8 @@ struct RemoteObject {
 
     DeviceState *dev;
     DeviceListener listener;
+    QIOChannel *ioregfd_ioc;
+    AioContext *ioregfd_ctx;
     GHashTable *ioregionfd_hash;
 };
 
diff --git a/linux-headers/ioregionfd.h b/linux-headers/ioregionfd.h
new file mode 100644
index 0000000000..58f9b5ba61
--- /dev/null
+++ b/linux-headers/ioregionfd.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: ((GPL-2.0-only WITH Linux-syscall-note) OR BSD-3-Clause) */
+#ifndef _UAPI_LINUX_IOREGION_H
+#define _UAPI_LINUX_IOREGION_H
+
+/* Wire protocol */
+
+struct ioregionfd_cmd {
+	__u8 cmd;
+	__u8 size_exponent : 4;
+	__u8 resp : 1;
+	__u8 padding[6];
+	__u64 user_data;
+	__u64 offset;
+	__u64 data;
+};
+
+struct ioregionfd_resp {
+	__u64 data;
+	__u8 pad[24];
+};
+
+#define IOREGIONFD_CMD_READ    0
+#define IOREGIONFD_CMD_WRITE   1
+
+#define IOREGIONFD_SIZE_8BIT   0
+#define IOREGIONFD_SIZE_16BIT  1
+#define IOREGIONFD_SIZE_32BIT  2
+#define IOREGIONFD_SIZE_64BIT  3
+
+#endif
diff --git a/hw/remote/ioregionfd.c b/hw/remote/ioregionfd.c
index 1d371357c6..dd04c39e25 100644
--- a/hw/remote/ioregionfd.c
+++ b/hw/remote/ioregionfd.c
@@ -26,6 +26,7 @@
 #include "hw/pci/pci.h"
 #include "qapi/qapi-visit-qom.h"
 #include "hw/remote/remote.h"
+#include "ioregionfd.h"
 
 #define TYPE_IOREGIONFD_OBJECT "ioregionfd-object"
 OBJECT_DECLARE_TYPE(IORegionFDObject, IORegionFDObjectClass, IOREGIONFD_OBJECT)
@@ -91,6 +92,116 @@ void ioregionfd_set_bar_type(GSList *list, uint32_t bar, bool memory)
     }
 }
 
+int qio_channel_ioregionfd_read(QIOChannel *ioc, gpointer opaque,
+                                Error **errp)
+{
+    struct RemoteObject *o = (struct RemoteObject *)opaque;
+    struct ioregionfd_cmd cmd = {};
+    struct iovec iov = {
+        .iov_base = &cmd,
+        .iov_len = sizeof(struct ioregionfd_cmd),
+    };
+    IORegionFDObject *ioregfd_obj;
+    PCIDevice *pci_dev;
+    hwaddr addr;
+    struct ioregionfd_resp resp = {};
+    int bar = 0;
+    Error *local_err = NULL;
+    uint64_t val = UINT64_MAX;
+    AddressSpace *as;
+    int ret = -EINVAL;
+
+    ERRP_GUARD();
+
+    if (!ioc) {
+        return -EINVAL;
+    }
+    ret = qio_channel_readv_full(ioc, &iov, 1, NULL, 0, &local_err);
+
+    if (ret == QIO_CHANNEL_ERR_BLOCK) {
+        return -EINVAL;
+    }
+
+    if (ret <= 0) {
+        /* read error or other side closed connection */
+        if (local_err) {
+            error_report_err(local_err);
+        }
+        error_setg(errp, "ioregionfd receive error");
+        return -EINVAL;
+    }
+
+    bar = cmd.user_data;
+    pci_dev = PCI_DEVICE(o->dev);
+    addr = (hwaddr)(pci_get_bar_addr(pci_dev, bar) + cmd.offset);
+    IORegionFDObject key = {.ioregfd = {.bar = bar} };
+    ioregfd_obj = g_hash_table_lookup(o->ioregionfd_hash, &key);
+
+    if (!ioregfd_obj) {
+        error_setg(errp, "Could not find IORegionFDObject");
+        return -EINVAL;
+    }
+    if (ioregfd_obj->ioregfd.memory) {
+        as = &address_space_memory;
+    } else {
+        as = &address_space_io;
+    }
+
+    if (ret > 0 && pci_dev) {
+        switch (cmd.cmd) {
+        case IOREGIONFD_CMD_READ:
+            ret = address_space_rw(as, addr, MEMTXATTRS_UNSPECIFIED,
+                                   (void *)&val, 1 << cmd.size_exponent,
+                                   false);
+            if (ret != MEMTX_OK) {
+                ret = -EINVAL;
+                error_setg(errp, "Bad address %"PRIx64" in mem read", addr);
+                val = UINT64_MAX;
+            }
+
+            memset(&resp, 0, sizeof(resp));
+            resp.data = val;
+            if (qio_channel_write_all(ioc, (char *)&resp, sizeof(resp),
+                                      &local_err)) {
+                error_propagate(errp, local_err);
+                goto fatal;
+            }
+            break;
+        case IOREGIONFD_CMD_WRITE:
+            ret = address_space_rw(as, addr, MEMTXATTRS_UNSPECIFIED,
+                                   (void *)&cmd.data, 1 << cmd.size_exponent,
+                                   true);
+            if (ret != MEMTX_OK) {
+                error_setg(errp, "Bad address %"PRIx64" for mem write", addr);
+                val = UINT64_MAX;
+            }
+
+            if (cmd.resp) {
+                memset(&resp, 0, sizeof(resp));
+                if (ret != MEMTX_OK) {
+                    resp.data = UINT64_MAX;
+                    ret = -EINVAL;
+                } else {
+                    resp.data = cmd.data;
+                }
+                if (qio_channel_write_all(ioc, (char *)&resp, sizeof(resp),
+                                          &local_err)) {
+                    error_propagate(errp, local_err);
+                    goto fatal;
+                }
+            }
+            break;
+        default:
+            error_setg(errp, "Unknown ioregionfd command from kvm");
+            break;
+        }
+    }
+    return ret;
+
+ fatal:
+    return -EINVAL;
+}
+
 static void ioregionfd_object_init(Object *obj)
 {
     IORegionFDObjectClass *k = IOREGIONFD_OBJECT_GET_CLASS(obj);
diff --git a/hw/remote/remote-obj.c b/hw/remote/remote-obj.c
index 46c2e2a5bd..2b005eab40 100644
--- a/hw/remote/remote-obj.c
+++ b/hw/remote/remote-obj.c
@@ -11,6 +11,7 @@
 #include "qemu-common.h"
 
 #include "qemu/error-report.h"
+#include "sysemu/iothread.h"
 #include "qemu/notify.h"
 #include "qom/object_interfaces.h"
 #include "hw/qdev-core.h"
@@ -78,6 +79,16 @@ static void remote_object_unrealize_listener(DeviceListener *listener,
     }
 }
 
+static IOThread *ioregionfd_iot;
+
+static void ioregion_read(void *opaque)
+{
+    struct RemoteObject *o = opaque;
+    Error *local_error = NULL;
+
+    qio_channel_ioregionfd_read(o->ioregfd_ioc, opaque, &local_error);
+}
+
 static GSList *ioregions_list;
 
 static unsigned int ioregionfd_bar_hash(const void *key)
@@ -104,6 +115,8 @@ static void ioregionfd_prepare_for_dev(RemoteObject *o, PCIDevice *dev)
 {
     IORegionFDObject *ioregfd_obj = NULL;
     GSList *obj_list, *list;
+    QIOChannel *ioc = NULL;
+    Error *local_err = NULL;
 
     list = ioregionfd_get_obj_list();
 
@@ -143,6 +156,30 @@ static void ioregionfd_prepare_for_dev(RemoteObject *o, PCIDevice *dev)
     /* This is default and will be changed when proxy requests region info. */
     ioregfd_obj->ioregfd.memory = true;
 
+    ioc = qio_channel_new_fd(ioregfd_obj->ioregfd.fd, &local_err);
+    if (!ioc) {
+        error_prepend(&local_err, "Could not create IOC channel for" \
+                      "ioregionfd fd %d", ioregfd_obj->ioregfd.fd);
+        error_report_err(local_err);
+        goto fatal;
+    }
+    o->ioregfd_ioc = ioc;
+
+    if (ioregionfd_iot == NULL) {
+        ioregionfd_iot = iothread_create("ioregionfd iothread",
+                                       &local_err);
+        if (local_err) {
+            qio_channel_shutdown(o->ioregfd_ioc, QIO_CHANNEL_SHUTDOWN_BOTH,
+                                 NULL);
+            qio_channel_close(o->ioregfd_ioc, NULL);
+            error_report_err(local_err);
+            goto fatal;
+        }
+    }
+    o->ioregfd_ctx = iothread_get_aio_context(ioregionfd_iot);
+    qio_channel_set_aio_fd_handler(o->ioregfd_ioc, o->ioregfd_ctx,
+                                   ioregion_read, NULL, o);
+
     ioregions_list = list;
     return;
 
@@ -238,8 +275,15 @@ static void remote_object_finalize(Object *obj)
 
     k->nr_devs--;
     g_free(o->devid);
+
+    iothread_destroy(ioregionfd_iot);
     /* Free the list of the ioregions. */
     g_slist_foreach(ioregions_list, ioregionfd_release, NULL);
+    if (o->ioregfd_ioc) {
+        qio_channel_shutdown(o->ioregfd_ioc, QIO_CHANNEL_SHUTDOWN_BOTH, NULL);
+        qio_channel_close(o->ioregfd_ioc, NULL);
+    }
+
     g_slist_free(ioregions_list);
     g_hash_table_destroy(o->ioregionfd_hash);
 }
