diff --git a/hw/i3c/aspeed_i3c.c b/hw/i3c/aspeed_i3c.c
index ffe0b21ca0..055ad3f7fd 100644
--- a/hw/i3c/aspeed_i3c.c
+++ b/hw/i3c/aspeed_i3c.c
@@ -20,6 +20,14 @@
 #include "hw/i3c/i3c.h"
 #include "hw/irq.h"
 
+/*
+ * Disable event command values. sent along with a DISEC CCC to disable certain
+ * events on targets.
+ */
+#define DISEC_HJ 0x08
+#define DISEC_CR 0x02
+#define DISEC_INT 0x01
+
 /* I3C Controller Registers */
 REG32(I3C1_REG0, 0x10)
 REG32(I3C1_REG1, 0x14)
@@ -444,6 +452,23 @@ static inline uint8_t aspeed_i3c_device_fifo_threshold_from_reg(uint8_t regval)
     return regval = regval ? (2 << regval) : 1;
 }
 
+static inline uint8_t aspeed_i3c_device_ibi_slice_size(AspeedI3CDevice *s)
+{
+    uint8_t ibi_slice_size = ARRAY_FIELD_EX32(s->regs, QUEUE_THLD_CTRL,
+                                              IBI_DATA_THLD);
+    /* The minimum supported slice size is 4 bytes. */
+    if (ibi_slice_size == 0) {
+        ibi_slice_size = 1;
+    }
+    ibi_slice_size *= sizeof(uint32_t);
+    /* maximum supported size is 63 bytes. */
+    if (ibi_slice_size >= 64) {
+        ibi_slice_size = 63;
+    }
+
+    return ibi_slice_size;
+}
+
 static void aspeed_i3c_device_update_irq(AspeedI3CDevice *s)
 {
     bool level = !!(s->regs[R_INTR_SIGNAL_EN] & s->regs[R_INTR_STATUS]);
@@ -591,6 +616,261 @@ static uint8_t aspeed_i3c_device_target_addr(AspeedI3CDevice *s,
                       DEV_DYNAMIC_ADDR);
 }
 
+static int aspeed_i3c_device_addr_table_index_from_addr(AspeedI3CDevice *s,
+                                                        uint8_t addr)
+{
+    uint8_t table_size = ARRAY_FIELD_EX32(s->regs, DEVICE_ADDR_TABLE_POINTER,
+                                          DEPTH);
+    for (uint8_t i = 0; i < table_size; i++) {
+        if (aspeed_i3c_device_target_addr(s, i) == addr) {
+            return i;
+        }
+    }
+    return -1;
+}
+
+static void aspeed_i3c_device_send_disec(AspeedI3CDevice *s)
+{
+    uint8_t ccc = I3C_CCC_DISEC;
+    if (s->ibi_data.send_direct_disec) {
+        ccc = I3C_CCCD_DISEC;
+    }
+
+    aspeed_i3c_device_send_start(s, I3C_BROADCAST, /*is_recv=*/false,
+                                 /*is_i2c=*/false);
+    aspeed_i3c_device_send_byte(s, ccc, /*is_i2c=*/false);
+    if (s->ibi_data.send_direct_disec) {
+        aspeed_i3c_device_send_start(s, s->ibi_data.disec_addr,
+                                     /*is_recv=*/false, /*is_i2c=*/false);
+    }
+    aspeed_i3c_device_send_byte(s, s->ibi_data.disec_byte, /*is_i2c=*/false);
+}
+
+static int aspeed_i3c_device_handle_hj(AspeedI3CDevice *s)
+{
+    if (ARRAY_FIELD_EX32(s->regs, IBI_QUEUE_CTRL, NOTIFY_REJECTED_HOT_JOIN)) {
+        s->ibi_data.notify_ibi_nack = true;
+    }
+
+    bool nack_and_disable = ARRAY_FIELD_EX32(s->regs, DEVICE_CTRL,
+                                             HOT_JOIN_ACK_NACK_CTRL);
+    if (nack_and_disable) {
+        s->ibi_data.ibi_queue_status = FIELD_DP32(s->ibi_data.ibi_queue_status,
+                                                  IBI_QUEUE_STATUS,
+                                                  IBI_STATUS, 1);
+        s->ibi_data.ibi_nacked = true;
+        s->ibi_data.disec_byte = DISEC_HJ;
+        return -1;
+    }
+    return 0;
+}
+
+static int aspeed_i3c_device_handle_ctlr_req(AspeedI3CDevice *s, uint8_t addr)
+{
+    if (ARRAY_FIELD_EX32(s->regs, IBI_QUEUE_CTRL, NOTIFY_REJECTED_MASTER_REQ)) {
+        s->ibi_data.notify_ibi_nack = true;
+    }
+
+    int table_offset = aspeed_i3c_device_addr_table_index_from_addr(s, addr);
+    /* Doesn't exist in the table, NACK it, don't DISEC. */
+    if (table_offset < 0) {
+        return -1;
+    }
+
+    table_offset += R_DEVICE_ADDR_TABLE_LOC1;
+    if (FIELD_EX32(s->regs[table_offset], DEVICE_ADDR_TABLE_LOC1, MR_REJECT)) {
+        s->ibi_data.ibi_queue_status = FIELD_DP32(s->ibi_data.ibi_queue_status,
+                                                  IBI_QUEUE_STATUS,
+                                                  IBI_STATUS, 1);
+        s->ibi_data.ibi_nacked = true;
+        s->ibi_data.disec_addr = addr;
+        /* Tell the requester to disable controller role requests. */
+        s->ibi_data.disec_byte = DISEC_CR;
+        s->ibi_data.send_direct_disec = true;
+        return -1;
+    }
+    return 0;
+}
+
+static int aspeed_i3c_device_handle_targ_irq(AspeedI3CDevice *s, uint8_t addr)
+{
+    if (ARRAY_FIELD_EX32(s->regs, IBI_QUEUE_CTRL, NOTIFY_REJECTED_SLAVE_IRQ)) {
+        s->ibi_data.notify_ibi_nack = true;
+    }
+
+    int table_offset = aspeed_i3c_device_addr_table_index_from_addr(s, addr);
+    /* Doesn't exist in the table, NACK it, don't DISEC. */
+    if (table_offset < 0) {
+        return -1;
+    }
+
+    table_offset += R_DEVICE_ADDR_TABLE_LOC1;
+    if (FIELD_EX32(s->regs[table_offset], DEVICE_ADDR_TABLE_LOC1, SIR_REJECT)) {
+        s->ibi_data.ibi_queue_status = FIELD_DP32(s->ibi_data.ibi_queue_status,
+                                                  IBI_QUEUE_STATUS,
+                                                  IBI_STATUS, 1);
+        s->ibi_data.ibi_nacked = true;
+        s->ibi_data.disec_addr = addr;
+        /* Tell the requester to disable interrupts. */
+        s->ibi_data.disec_byte = DISEC_INT;
+        s->ibi_data.send_direct_disec = true;
+        return -1;
+    }
+    return 0;
+}
+
+static int aspeed_i3c_device_ibi_handle(I3CBus *bus, I3CTarget *target,
+                                        uint8_t addr, bool is_recv)
+{
+    AspeedI3CDevice *s = ASPEED_I3C_DEVICE(bus->qbus.parent);
+
+    trace_aspeed_i3c_device_ibi_handle(s->id, addr, is_recv);
+    s->ibi_data.ibi_queue_status = FIELD_DP32(s->ibi_data.ibi_queue_status,
+                                              IBI_QUEUE_STATUS, IBI_ID,
+                                              (addr << 1) | is_recv);
+    /* Is this a hot join request? */
+    if (addr == I3C_HJ_ADDR) {
+        return aspeed_i3c_device_handle_hj(s);
+    }
+    /* Is secondary controller requesting access? */
+    if (addr == target->address && !is_recv) {
+        return aspeed_i3c_device_handle_ctlr_req(s, addr);
+    }
+    /* Is this a target IRQ? */
+    if (addr == target->address && is_recv) {
+        return aspeed_i3c_device_handle_targ_irq(s, addr);
+    }
+
+    /* Not sure what this is, NACK it. */
+    return -1;
+}
+
+static int aspeed_i3c_device_ibi_recv(I3CBus *bus, uint8_t data)
+{
+    AspeedI3CDevice *s = ASPEED_I3C_DEVICE(bus->qbus.parent);
+    if (fifo8_is_full(&s->ibi_data.ibi_intermediate_queue)) {
+        return -1;
+    }
+
+    fifo8_push(&s->ibi_data.ibi_intermediate_queue, data);
+    trace_aspeed_i3c_device_ibi_recv(s->id, data);
+    return 0;
+}
+
+static void aspeed_i3c_device_ibi_queue_push(AspeedI3CDevice *s)
+{
+    /* Stored value is in 32-bit chunks, convert it to byte chunks. */
+    uint8_t ibi_slice_size = aspeed_i3c_device_ibi_slice_size(s);
+    uint8_t num_slices = fifo8_num_used(&s->ibi_data.ibi_intermediate_queue) /
+                         ibi_slice_size;
+    uint8_t ibi_status_count = num_slices;
+    union {
+        uint8_t b[sizeof(uint32_t)];
+        uint32_t val32;
+    } ibi_data = {
+        .val32 = 0
+    };
+
+    /* The report was suppressed, do nothing. */
+    if (s->ibi_data.ibi_nacked && !s->ibi_data.notify_ibi_nack) {
+        ARRAY_FIELD_DP32(s->regs, PRESENT_STATE, CM_TFR_ST_STATUS,
+                         ASPEED_I3C_TRANSFER_STATE_IDLE);
+        ARRAY_FIELD_DP32(s->regs, PRESENT_STATE, CM_TFR_STATUS,
+                         ASPEED_I3C_TRANSFER_STATUS_IDLE);
+        return;
+    }
+
+    /* If we don't have any slices to push, just push the status. */
+    if (num_slices == 0) {
+        s->ibi_data.ibi_queue_status =
+             FIELD_DP32(s->ibi_data.ibi_queue_status, IBI_QUEUE_STATUS,
+                        LAST_STATUS, 1);
+        fifo32_push(&s->ibi_queue, s->ibi_data.ibi_queue_status);
+        ibi_status_count = 1;
+    }
+
+    for (uint8_t i = 0; i < num_slices; i++) {
+        /* If this is the last slice, set LAST_STATUS. */
+        if (fifo8_num_used(&s->ibi_data.ibi_intermediate_queue) <
+            ibi_slice_size) {
+            s->ibi_data.ibi_queue_status =
+                FIELD_DP32(s->ibi_data.ibi_queue_status, IBI_QUEUE_STATUS,
+                           IBI_DATA_LEN,
+                           fifo8_num_used(&s->ibi_data.ibi_intermediate_queue));
+            s->ibi_data.ibi_queue_status =
+                FIELD_DP32(s->ibi_data.ibi_queue_status, IBI_QUEUE_STATUS,
+                           LAST_STATUS, 1);
+        } else {
+            s->ibi_data.ibi_queue_status =
+                FIELD_DP32(s->ibi_data.ibi_queue_status, IBI_QUEUE_STATUS,
+                           IBI_DATA_LEN, ibi_slice_size);
+        }
+
+        /* Push the IBI status header. */
+        fifo32_push(&s->ibi_queue, s->ibi_data.ibi_queue_status);
+        /* Move each IBI byte into a 32-bit word and push it into the queue. */
+        for (uint8_t j = 0; j < ibi_slice_size; ++j) {
+            if (fifo8_is_empty(&s->ibi_data.ibi_intermediate_queue)) {
+                break;
+            }
+
+            ibi_data.b[j & 3] = fifo8_pop(&s->ibi_data.ibi_intermediate_queue);
+            /* We have 32-bits, push it to the IBI FIFO. */
+            if ((j & 0x03) == 0x03) {
+                fifo32_push(&s->ibi_queue, ibi_data.val32);
+                ibi_data.val32 = 0;
+            }
+        }
+        /* If the data isn't 32-bit aligned, push the leftover bytes. */
+        if (ibi_slice_size & 0x03) {
+            fifo32_push(&s->ibi_queue, ibi_data.val32);
+        }
+
+        /* Clear out the data length for the next iteration. */
+        s->ibi_data.ibi_queue_status = FIELD_DP32(s->ibi_data.ibi_queue_status,
+                                         IBI_QUEUE_STATUS, IBI_DATA_LEN, 0);
+    }
+
+    ARRAY_FIELD_DP32(s->regs, QUEUE_STATUS_LEVEL, IBI_BUF_BLR,
+                     fifo32_num_used(&s->ibi_queue));
+    ARRAY_FIELD_DP32(s->regs, QUEUE_STATUS_LEVEL, IBI_STATUS_CNT,
+                     ibi_status_count);
+    /* Threshold is the register value + 1. */
+    uint8_t threshold = ARRAY_FIELD_EX32(s->regs, QUEUE_THLD_CTRL,
+                                         IBI_STATUS_THLD) + 1;
+    if (fifo32_num_used(&s->ibi_queue) >= threshold) {
+        ARRAY_FIELD_DP32(s->regs, INTR_STATUS, IBI_THLD, 1);
+        aspeed_i3c_device_update_irq(s);
+    }
+
+    /* State update. */
+    ARRAY_FIELD_DP32(s->regs, PRESENT_STATE, CM_TFR_ST_STATUS,
+                     ASPEED_I3C_TRANSFER_STATE_IDLE);
+    ARRAY_FIELD_DP32(s->regs, PRESENT_STATE, CM_TFR_STATUS,
+                     ASPEED_I3C_TRANSFER_STATUS_IDLE);
+}
+
+static int aspeed_i3c_device_ibi_finish(I3CBus *bus)
+{
+    AspeedI3CDevice *s = ASPEED_I3C_DEVICE(bus->qbus.parent);
+    bool nack_and_disable_hj = ARRAY_FIELD_EX32(s->regs, DEVICE_CTRL,
+                                                HOT_JOIN_ACK_NACK_CTRL);
+    if (nack_and_disable_hj || s->ibi_data.send_direct_disec) {
+        aspeed_i3c_device_send_disec(s);
+    }
+    aspeed_i3c_device_ibi_queue_push(s);
+
+    /* Clear out the intermediate values. */
+    s->ibi_data.ibi_queue_status = 0;
+    s->ibi_data.disec_addr = 0;
+    s->ibi_data.disec_byte = 0;
+    s->ibi_data.send_direct_disec = false;
+    s->ibi_data.notify_ibi_nack = false;
+    s->ibi_data.ibi_nacked = false;
+
+    return 0;
+}
+
 static uint32_t aspeed_i3c_device_intr_status_r(AspeedI3CDevice *s)
 {
     /* Only return the status whose corresponding EN bits are set. */
@@ -650,6 +930,25 @@ static uint32_t aspeed_i3c_device_pop_rx(AspeedI3CDevice *s)
     return val;
 }
 
+static uint32_t aspeed_i3c_device_ibi_queue_r(AspeedI3CDevice *s)
+{
+    if (fifo32_is_empty(&s->ibi_queue)) {
+        return 0;
+    }
+
+    uint32_t val = fifo32_pop(&s->ibi_queue);
+    ARRAY_FIELD_DP32(s->regs, QUEUE_STATUS_LEVEL, IBI_BUF_BLR,
+                     fifo32_num_used(&s->ibi_queue));
+    /* Threshold is the register value + 1. */
+    uint8_t threshold = ARRAY_FIELD_EX32(s->regs, QUEUE_THLD_CTRL,
+                                         IBI_STATUS_THLD) + 1;
+    if (fifo32_num_used(&s->ibi_queue) < threshold) {
+        ARRAY_FIELD_DP32(s->regs, INTR_STATUS, IBI_THLD, 0);
+        aspeed_i3c_device_update_irq(s);
+    }
+    return val;
+}
+
 static uint32_t aspeed_i3c_device_resp_queue_port_r(AspeedI3CDevice *s)
 {
     if (fifo32_is_empty(&s->resp_queue)) {
@@ -687,6 +986,9 @@ static uint64_t aspeed_i3c_device_read(void *opaque, hwaddr offset,
     case R_INTR_FORCE:
         value = 0;
         break;
+    case R_IBI_QUEUE_DATA:
+        value = aspeed_i3c_device_ibi_queue_r(s);
+        break;
     case R_INTR_STATUS:
         value = aspeed_i3c_device_intr_status_r(s);
         break;
@@ -1412,8 +1714,16 @@ static void aspeed_i3c_device_realize(DeviceState *dev, Error **errp)
     fifo32_create(&s->resp_queue, ASPEED_I3C_RESP_QUEUE_CAPACITY);
     fifo32_create(&s->tx_queue, ASPEED_I3C_TX_QUEUE_CAPACITY);
     fifo32_create(&s->rx_queue, ASPEED_I3C_RX_QUEUE_CAPACITY);
+    fifo32_create(&s->ibi_queue, ASPEED_I3C_IBI_QUEUE_CAPACITY);
+    /* Arbitrarily large enough to not be an issue. */
+    fifo8_create(&s->ibi_data.ibi_intermediate_queue,
+                  ASPEED_I3C_IBI_QUEUE_CAPACITY * 8);
 
     s->bus = i3c_init_bus(DEVICE(s), name);
+    I3CBusClass *bc = I3C_BUS_GET_CLASS(s->bus);
+    bc->ibi_handle = aspeed_i3c_device_ibi_handle;
+    bc->ibi_recv = aspeed_i3c_device_ibi_recv;
+    bc->ibi_finish = aspeed_i3c_device_ibi_finish;
 }
 
 static uint64_t aspeed_i3c_read(void *opaque, hwaddr addr, unsigned int size)
diff --git a/hw/i3c/trace-events b/hw/i3c/trace-events
index 370c08ea45..7a202b28c9 100644
--- a/hw/i3c/trace-events
+++ b/hw/i3c/trace-events
@@ -7,6 +7,8 @@ aspeed_i3c_device_read(uint32_t deviceid, uint64_t offset, uint64_t data) "I3C D
 aspeed_i3c_device_write(uint32_t deviceid, uint64_t offset, uint64_t data) "I3C Dev[%u] write: offset 0x%" PRIx64 " data 0x%" PRIx64
 aspeed_i3c_device_send(uint32_t deviceid, uint32_t num_bytes) "I3C Dev[%u] send %" PRId32 " bytes to bus"
 aspeed_i3c_device_recv_data(uint32_t deviceid, uint32_t num_bytes) "I3C Dev[%u] recv %" PRId32 " bytes from bus"
+aspeed_i3c_device_ibi_recv(uint32_t deviceid, uint8_t ibi_byte) "I3C Dev[%u] recv IBI byte 0x%" PRIx8
+aspeed_i3c_device_ibi_handle(uint32_t deviceid, uint8_t addr, bool rnw) "I3C Dev[%u] handle IBI from address 0x%" PRIx8 " RnW=%d"
 aspeed_i3c_device_pop_rx(uint32_t deviceid, uint32_t data) "I3C Dev[%u] pop 0x%" PRIx32 " from RX FIFO"
 aspeed_i3c_device_resp_queue_push(uint32_t deviceid, uint32_t data) "I3C Dev[%u] push 0x%" PRIx32 " to response queue"
 aspeed_i3c_device_push_tx(uint32_t deviceid, uint32_t data) "I3C Dev[%u] push 0x%" PRIx32 " to TX FIFO"
diff --git a/include/hw/i3c/aspeed_i3c.h b/include/hw/i3c/aspeed_i3c.h
index 55f2f45915..d2f0739b79 100644
--- a/include/hw/i3c/aspeed_i3c.h
+++ b/include/hw/i3c/aspeed_i3c.h
@@ -27,6 +27,7 @@ OBJECT_DECLARE_TYPE(AspeedI3CState, AspeedI3CClass, ASPEED_I3C)
 #define ASPEED_I3C_RESP_QUEUE_CAPACITY 0x10
 #define ASPEED_I3C_TX_QUEUE_CAPACITY   0x40
 #define ASPEED_I3C_RX_QUEUE_CAPACITY   0x40
+#define ASPEED_I3C_IBI_QUEUE_CAPACITY  0x10
 
 /* From datasheet. */
 #define ASPEED_I3C_CMD_ATTR_TRANSFER_CMD 0
@@ -146,6 +147,28 @@ typedef union AspeedI3CCmdQueueData {
     AspeedI3CAddrAssignCmd addr_assign_cmd;
 } AspeedI3CCmdQueueData;
 
+/*
+ * When we receive an IBI with data, we need to store it temporarily until
+ * the target is finished sending data. Then we can set the IBI queue status
+ * appropriately.
+ */
+typedef struct AspeedI3CDeviceIBIData {
+    /* Do we notify the user that an IBI was NACKed? */
+    bool notify_ibi_nack;
+    /* Intermediate storage of IBI_QUEUE_STATUS. */
+    uint32_t ibi_queue_status;
+    /* Temporary buffer to store IBI data from the target. */
+    Fifo8 ibi_intermediate_queue;
+    /* The address we should send a CCC_DISEC to. */
+    uint8_t disec_addr;
+    /* The byte we should send along with the CCC_DISEC. */
+    uint8_t disec_byte;
+    /* Should we send a direct DISEC CCC? (As opposed to global). */
+    bool send_direct_disec;
+    /* Was this IBI NACKed? */
+    bool ibi_nacked;
+} AspeedI3CDeviceIBIData;
+
 OBJECT_DECLARE_SIMPLE_TYPE(AspeedI3CDevice, ASPEED_I3C_DEVICE)
 typedef struct AspeedI3CDevice {
     /* <private> */
@@ -160,6 +183,10 @@ typedef struct AspeedI3CDevice {
     Fifo32 resp_queue;
     Fifo32 tx_queue;
     Fifo32 rx_queue;
+    Fifo32 ibi_queue;
+
+    /* Temporary storage for IBI data. */
+    AspeedI3CDeviceIBIData ibi_data;
 
     uint8_t id;
     uint32_t regs[ASPEED_I3C_DEVICE_NR_REGS];
