Add experimental properties x-hold-tag and x-hold-mode to ufs-pci and ufs-sysbus devices to facilitate end-to-end testing of host driver error handling and task abort recovery routines.
When x-hold-tag is set to a specific tag (0..255), the controller holds (delays completing) the matching transfer request until a Task Management Request (TMR) targeting the held request is issued by the host driver. If x-hold-tag=0xfffffffe (UFS_HOLD_TAG_ANY), the controller automatically intercepts the first READ_10/WRITE_10 request with LBA >= 512, avoiding guest boot-time metadata requests. The default value is 0xffffffff (UFS_HOLD_TAG_NONE), which disables fault injection while preserving full testability of Tag 0. The x-hold-mode property controls how the held request and subsequent TMR are resolved: - "abort-success": TMR ABORT_TASK aborts the held request, clears its state, and returns SUCCESS (COMPL). - "abort-failed": TMR ABORT_TASK returns TASK_MANAGEMENT_FUNC_FAILED. - "in-transition": TMR QUERY_TASK / ABORT_TASK completes the held request successfully and returns COMPL (simulating a command completing in transition). - "timeout": TMR ABORT_TASK does not complete, triggering TMR timeout. Both properties allow runtime modification on realized devices via QOM (e.g., QMP qom-set), enabling continuous, multi-case fault injection testing within a single running VM without requiring reboot cycles. Together with the companion Linux kernel patch: "[PATCH] scsi: ufs: core: Add fault injection for task abort failures" Link: https://lore.kernel.org/r/[email protected]/ these QEMU changes provide a deterministic testbed to verify UFS exception cases and ensure controller/driver health. This setup is particularly useful for establishing automated CI/CD pipelines to prevent regressions across future Linux kernel UFS driver and QEMU emulation changes. Signed-off-by: Stanley Jhu <[email protected]> --- hw/ufs/trace-events | 3 + hw/ufs/ufs-pci.c | 4 + hw/ufs/ufs-sysbus.c | 4 + hw/ufs/ufs.c | 289 +++++++++++++++++++++++++++++++++++++++++--- hw/ufs/ufs.h | 54 +++++++++ 5 files changed, 339 insertions(+), 15 deletions(-) diff --git a/hw/ufs/trace-events b/hw/ufs/trace-events index 5e5a54a3fb..a2c8f80811 100644 --- a/hw/ufs/trace-events +++ b/hw/ufs/trace-events @@ -16,6 +16,9 @@ ufs_mcq_create_sq(uint8_t sqid, uint8_t cqid, uint64_t addr, uint16_t size) "mcq ufs_mcq_create_cq(uint8_t cqid, uint64_t addr, uint16_t size) "mcq create cq cqid %"PRIu8", addr 0x%"PRIx64", size %"PRIu16"" ufs_write_mcq_op_reg(uint8_t qid, uint32_t offset, uint32_t data) "qid %"PRIu8", offset 0x%"PRIx32", data 0x%"PRIx32"" ufs_process_tmr(uint8_t func, uint32_t tag, uint8_t resp) "query_func 0x%"PRIx8", task_tag %"PRIu32", tm_resp 0x%"PRIx8"" +ufs_inject_hold_req(uint8_t tag, uint32_t lba) "held command tag %"PRIu8", lba %"PRIu32"" +ufs_set_hold_tag(uint32_t tag) "hold-tag set to 0x%"PRIx32"" +ufs_set_hold_mode(const char *mode) "hold-mode set to %s" ufs_hce_reset(void) "HCE 1 -> 0 reset: cancelling BHs, resetting MCQ and request lists" # error condition diff --git a/hw/ufs/ufs-pci.c b/hw/ufs/ufs-pci.c index 8abd7d98e3..fe6c497dc2 100644 --- a/hw/ufs/ufs-pci.c +++ b/hw/ufs/ufs-pci.c @@ -72,6 +72,10 @@ static const Property ufs_pci_props[] = { 0x400), DEFINE_PROP_UINT32("wb-min-size", UfsPciState, ufs.params.wb_min_size, 0x100), + DEFINE_PROP_UNSIGNED("x-hold-tag", UfsPciState, ufs.params.x_hold_tag, + UFS_HOLD_TAG_NONE, ufs_prop_hold_tag, uint32_t), + DEFINE_PROP("x-hold-mode", UfsPciState, ufs.params.x_hold_mode, + ufs_prop_hold_mode, char *), }; static const VMStateDescription ufs_pci_vmstate = { diff --git a/hw/ufs/ufs-sysbus.c b/hw/ufs/ufs-sysbus.c index 84de2e95ac..f0e4dfcd71 100644 --- a/hw/ufs/ufs-sysbus.c +++ b/hw/ufs/ufs-sysbus.c @@ -45,6 +45,10 @@ static const Property ufs_sysbus_props[] = { 0x400), DEFINE_PROP_UINT32("wb-min-size", SysbusUfsState, ufs.params.wb_min_size, 0x100), + DEFINE_PROP_UNSIGNED("x-hold-tag", SysbusUfsState, ufs.params.x_hold_tag, + UFS_HOLD_TAG_NONE, ufs_prop_hold_tag, uint32_t), + DEFINE_PROP("x-hold-mode", SysbusUfsState, ufs.params.x_hold_mode, + ufs_prop_hold_mode, char *), }; static const VMStateDescription ufs_sysbus_vmstate = { diff --git a/hw/ufs/ufs.c b/hw/ufs/ufs.c index c7064f1825..2c3d608c1e 100644 --- a/hw/ufs/ufs.c +++ b/hw/ufs/ufs.c @@ -15,6 +15,7 @@ #include "qemu/osdep.h" #include "qapi/error.h" +#include "qapi/visitor.h" #include "scsi/constants.h" #include "hw/core/irq.h" #include "trace.h" @@ -816,6 +817,14 @@ static void ufs_hce_reset(UfsHc *u) memset(u->mcq_op_reg, 0, sizeof(u->mcq_op_reg)); } + if (u->held_req) { + u->held_req = NULL; + u->active_hold_tag = UFS_HOLD_TAG_NONE; + u->params.x_hold_tag = UFS_HOLD_TAG_NONE; + } else { + u->active_hold_tag = u->params.x_hold_tag; + } + u->resetting = false; /* 5. De-assert IRQ */ @@ -873,27 +882,95 @@ static void ufs_process_tmr(UfsHc *u, uint32_t val) task_tag = be32_to_cpu(desc.upiu_req.input_param2); if (tm_func == UFS_QUERY_TASK) { - UfsRequest *req = ufs_find_req_by_tag(u, task_tag); - - if (req) { - tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_SUCCEEDED; + if (u->held_req && task_tag == u->active_hold_tag) { + if (u->hold_mode == UFS_HOLD_IN_TRANSITION) { + UfsRequest *hreq = u->held_req; + u->held_req = NULL; + u->active_hold_tag = UFS_HOLD_TAG_NONE; + u->params.x_hold_tag = UFS_HOLD_TAG_NONE; + tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_COMPL; + if (hreq) { + uint16_t data_seg_len = + sizeof(hreq->rsp_upiu.sr.sense_data_len); + hreq->rsp_upiu.sr.sense_data_len = 0; + hreq->rsp_upiu.sr.residual_transfer_count = 0; + ufs_build_upiu_header(hreq, + UFS_UPIU_TRANSACTION_RESPONSE, + 0, + UFS_COMMAND_RESULT_SUCCESS, + 0, + data_seg_len); + ufs_complete_req(hreq, UFS_REQUEST_SUCCESS); + } + } else { + tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_SUCCEEDED; + } } else { - tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_COMPL; + UfsRequest *req = ufs_find_req_by_tag(u, task_tag); + + if (req) { + tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_SUCCEEDED; + } else { + tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_COMPL; + } } } else if (tm_func == UFS_ABORT_TASK) { - UfsRequest *req = ufs_find_req_by_tag(u, task_tag); - - if (req) { - ufs_clear_req(req); - req->state = UFS_REQUEST_IDLE; - if (ufs_mcq_req(req)) { - QTAILQ_INSERT_TAIL(&req->sq->req_list, req, entry); - qemu_bh_schedule(req->sq->bh); + if (u->held_req && task_tag == u->active_hold_tag) { + if (u->hold_mode == UFS_HOLD_ABORT_FAILED) { + tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_FAILED; + } else if (u->hold_mode == UFS_HOLD_TIMEOUT) { + continue; + } else if (u->hold_mode == UFS_HOLD_IN_TRANSITION) { + UfsRequest *hreq = u->held_req; + u->held_req = NULL; + u->active_hold_tag = UFS_HOLD_TAG_NONE; + u->params.x_hold_tag = UFS_HOLD_TAG_NONE; + tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_COMPL; + if (hreq) { + uint16_t data_seg_len = + sizeof(hreq->rsp_upiu.sr.sense_data_len); + hreq->rsp_upiu.sr.sense_data_len = 0; + hreq->rsp_upiu.sr.residual_transfer_count = 0; + ufs_build_upiu_header(hreq, + UFS_UPIU_TRANSACTION_RESPONSE, + 0, + UFS_COMMAND_RESULT_SUCCESS, + 0, + data_seg_len); + ufs_complete_req(hreq, UFS_REQUEST_SUCCESS); + } } else { - u->reg.utrldbr &= ~(1 << req->slot); + /* UFS_HOLD_ABORT_SUCCESS or default */ + UfsRequest *hreq = u->held_req; + u->held_req = NULL; + ufs_clear_req(hreq); + hreq->state = UFS_REQUEST_IDLE; + if (ufs_mcq_req(hreq)) { + QTAILQ_INSERT_TAIL(&hreq->sq->req_list, + hreq, entry); + qemu_bh_schedule(hreq->sq->bh); + } else { + u->reg.utrldbr &= ~(1 << hreq->slot); + } + u->active_hold_tag = UFS_HOLD_TAG_NONE; + u->params.x_hold_tag = UFS_HOLD_TAG_NONE; + tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_COMPL; + } + } else { + UfsRequest *req = ufs_find_req_by_tag(u, task_tag); + + if (req) { + ufs_clear_req(req); + req->state = UFS_REQUEST_IDLE; + if (ufs_mcq_req(req)) { + QTAILQ_INSERT_TAIL(&req->sq->req_list, req, entry); + qemu_bh_schedule(req->sq->bh); + } else { + u->reg.utrldbr &= ~(1 << req->slot); + } } + tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_COMPL; } - tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_COMPL; } else { tm_resp = UFS_UPIU_TASK_MANAGEMENT_FUNC_NOT_SUPPORTED; } @@ -2323,6 +2400,42 @@ static void ufs_exec_req(UfsRequest *req) return; } + if (unlikely(req->hc->active_hold_tag != UFS_HOLD_TAG_NONE)) { + if (req->hc->active_hold_tag == UFS_HOLD_TAG_ANY) { + if (req->req_upiu.header.trans_type == + UFS_UPIU_TRANSACTION_COMMAND) { + uint8_t op = req->req_upiu.sc.cdb[0]; + + if (op == READ_10 || op == WRITE_10) { + /* + * In 10-byte SCSI read/write commands (SBC-4), bytes 2..5 + * encode the 32-bit Logical Block Address (LBA) in + * big-endian. + */ + uint32_t lba = ldl_be_p(&req->req_upiu.sc.cdb[2]); + + /* + * Only intercept target I/O targeting LBA >= + * UFS_HOLD_MIN_LBA. Skips guest boot-time partition table + * and superblock scanning. + */ + if (lba >= UFS_HOLD_MIN_LBA) { + req->hc->active_hold_tag = + req->req_upiu.header.task_tag; + req->hc->held_req = req; + trace_ufs_inject_hold_req(req->req_upiu.header.task_tag, + lba); + return; + } + } + } + } else if (req->req_upiu.header.task_tag == req->hc->active_hold_tag) { + req->hc->held_req = req; + trace_ufs_inject_hold_req(req->req_upiu.header.task_tag, 0); + return; + } + } + switch (req->req_upiu.header.trans_type) { case UFS_UPIU_TRANSACTION_NOP_OUT: req_result = ufs_exec_nop_cmd(req); @@ -2942,8 +3055,140 @@ static void ufs_init_hc(UfsHc *u) timer_init_ms(&u->idle_timer, QEMU_CLOCK_VIRTUAL_RT, ufs_idle_timer_cb, u); timer_mod(&u->idle_timer, now + UFS_IDLE_TIMER_TICK); + + u->active_hold_tag = u->params.x_hold_tag; + u->held_req = NULL; +} + +static void ufs_prop_get_hold_tag(Object *obj, Visitor *v, const char *name, + void *opaque, Error **errp) +{ + Property *prop = opaque; + uint32_t *ptr = object_field_prop_ptr(obj, prop); + UfsParams *params = container_of(ptr, UfsParams, x_hold_tag); + UfsHc *u = container_of(params, UfsHc, params); + + visit_type_uint32(v, name, &u->active_hold_tag, errp); +} + +static void ufs_prop_set_hold_tag(Object *obj, Visitor *v, const char *name, + void *opaque, Error **errp) +{ + Property *prop = opaque; + uint32_t *ptr = object_field_prop_ptr(obj, prop); + uint32_t val; + + if (!visit_type_uint32(v, name, &val, errp)) { + return; + } + + *ptr = val; + UfsParams *params = container_of(ptr, UfsParams, x_hold_tag); + UfsHc *u = container_of(params, UfsHc, params); + u->active_hold_tag = val; + trace_ufs_set_hold_tag(val); +} + +static void ufs_prop_set_default_hold_tag(ObjectProperty *op, + const Property *prop) +{ + object_property_set_default_uint(op, prop->defval.u); } +const PropertyInfo ufs_prop_hold_tag = { + .type = "uint32", + .description = "Task tag to hold for fault injection", + .get = ufs_prop_get_hold_tag, + .set = ufs_prop_set_hold_tag, + .set_default_value = ufs_prop_set_default_hold_tag, + .realized_set_allowed = true, +}; + +static void ufs_prop_get_hold_mode(Object *obj, Visitor *v, const char *name, + void *opaque, Error **errp) +{ + Property *prop = opaque; + char **ptr = object_field_prop_ptr(obj, prop); + UfsParams *params = container_of(ptr, UfsParams, x_hold_mode); + UfsHc *u = container_of(params, UfsHc, params); + const char *str; + char *val; + + switch (u->hold_mode) { + case UFS_HOLD_ABORT_FAILED: + str = "abort-failed"; + break; + case UFS_HOLD_IN_TRANSITION: + str = "in-transition"; + break; + case UFS_HOLD_TIMEOUT: + str = "timeout"; + break; + case UFS_HOLD_ABORT_SUCCESS: + default: + str = "abort-success"; + break; + } + + val = g_strdup(str); + visit_type_str(v, name, &val, errp); + g_free(val); +} + +static void ufs_prop_set_hold_mode(Object *obj, Visitor *v, const char *name, + void *opaque, Error **errp) +{ + Property *prop = opaque; + char **ptr = object_field_prop_ptr(obj, prop); + char *str; + UfsHoldMode mode; + + if (!visit_type_str(v, name, &str, errp)) { + return; + } + + if (!str || g_strcmp0(str, "abort-success") == 0) { + mode = UFS_HOLD_ABORT_SUCCESS; + } else if (g_strcmp0(str, "abort-failed") == 0) { + mode = UFS_HOLD_ABORT_FAILED; + } else if (g_strcmp0(str, "in-transition") == 0) { + mode = UFS_HOLD_IN_TRANSITION; + } else if (g_strcmp0(str, "timeout") == 0) { + mode = UFS_HOLD_TIMEOUT; + } else { + error_setg(errp, "invalid x-hold-mode: %s", str); + g_free(str); + return; + } + + g_free(*ptr); + *ptr = str; + UfsParams *params = container_of(ptr, UfsParams, x_hold_mode); + UfsHc *u = container_of(params, UfsHc, params); + u->hold_mode = mode; + trace_ufs_set_hold_mode(str ? str : "abort-success"); +} + +static void ufs_prop_release_hold_mode(Object *obj, const char *name, + void *opaque) +{ + Property *prop = opaque; + char **ptr = object_field_prop_ptr(obj, prop); + + g_free(*ptr); + *ptr = NULL; +} + +const PropertyInfo ufs_prop_hold_mode = { + .type = "str", + .description = "Fault injection mode: abort-success, abort-failed, " + "in-transition, timeout", + .get = ufs_prop_get_hold_mode, + .set = ufs_prop_set_hold_mode, + .release = ufs_prop_release_hold_mode, + .realized_set_allowed = true, +}; + bool ufs_realize(UfsHc *u, DeviceState *dev, AddressSpace *dma_as, Error **errp) { u->dev = dev; @@ -2953,6 +3198,20 @@ bool ufs_realize(UfsHc *u, DeviceState *dev, AddressSpace *dma_as, Error **errp) return false; } + if (!u->params.x_hold_mode || + g_strcmp0(u->params.x_hold_mode, "abort-success") == 0) { + u->hold_mode = UFS_HOLD_ABORT_SUCCESS; + } else if (g_strcmp0(u->params.x_hold_mode, "abort-failed") == 0) { + u->hold_mode = UFS_HOLD_ABORT_FAILED; + } else if (g_strcmp0(u->params.x_hold_mode, "in-transition") == 0) { + u->hold_mode = UFS_HOLD_IN_TRANSITION; + } else if (g_strcmp0(u->params.x_hold_mode, "timeout") == 0) { + u->hold_mode = UFS_HOLD_TIMEOUT; + } else { + error_setg(errp, "invalid x-hold-mode: %s", u->params.x_hold_mode); + return false; + } + qbus_init(&u->bus, sizeof(UfsBus), TYPE_UFS_BUS, dev, dev->id); u->bus.hc = u; diff --git a/hw/ufs/ufs.h b/hw/ufs/ufs.h index 265a43faaa..6edeab9db9 100644 --- a/hw/ufs/ufs.h +++ b/hw/ufs/ufs.h @@ -12,6 +12,7 @@ #define HW_UFS_UFS_H #include "hw/core/qdev.h" +#include "hw/core/qdev-properties.h" #include "hw/scsi/scsi.h" #include "block/ufs.h" #include "scsi/constants.h" @@ -88,6 +89,48 @@ typedef struct UfsLu { UfsScsiOp scsi_op; } UfsLu; +/* + * Fault injection modes for x-hold-mode property. + * Controls how the held transfer request and subsequent Task Management + * Request (TMR) are resolved by the controller: + * + * UFS_HOLD_ABORT_SUCCESS: TMR ABORT_TASK aborts the held request, clears its + * state, and returns SUCCESS (COMPL). + * UFS_HOLD_ABORT_FAILED: TMR ABORT_TASK returns TASK_MANAGEMENT_FUNC_FAILED, + * forcing host driver to escalate to Host Reset. + * UFS_HOLD_IN_TRANSITION: TMR QUERY_TASK / ABORT_TASK completes the held + * request successfully and returns COMPL (simulating + * a command completing in transition / grace period). + * UFS_HOLD_TIMEOUT: TMR ABORT_TASK does not complete or clear doorbell, + * triggering hardware-level TMR timeout. + */ +typedef enum UfsHoldMode { + UFS_HOLD_ABORT_SUCCESS = 0, + UFS_HOLD_ABORT_FAILED, + UFS_HOLD_IN_TRANSITION, + UFS_HOLD_TIMEOUT, +} UfsHoldMode; + +/* + * Sentinel values for x-hold-tag property. + * Task Tags in UFS are 8-bit (0..255). Values >= 256 are reserved as + * sentinel flags to preserve full testability of Tag 0. + * + * UFS_HOLD_TAG_NONE: Disable fault injection (default). + * UFS_HOLD_TAG_ANY: Automatically intercept the first user-space READ_10 + * or WRITE_10 command targeting LBA >= UFS_HOLD_MIN_LBA. + */ +#define UFS_HOLD_TAG_NONE 0xffffffff +#define UFS_HOLD_TAG_ANY 0xfffffffe + +/* + * Minimum Logical Block Address (LBA) for automatic fault injection. + * Skip early disk blocks (LBA 0..511, e.g. MBR, GPT partition tables, EFI, + * and filesystem superblocks) to ensure guest OS boot-time disk discovery + * and partition scanning complete cleanly without being held. + */ +#define UFS_HOLD_MIN_LBA 512 + typedef struct UfsParams { char *serial; uint8_t nutrs; /* Number of UTP Transfer Request Slots */ @@ -97,6 +140,8 @@ typedef struct UfsParams { uint8_t mcq_maxq; /* MCQ Maximum number of Queues */ uint32_t wb_max_size; /* WB Maximum allocation units */ uint32_t wb_min_size; /* WB Minimum allocation units */ + uint32_t x_hold_tag; + char *x_hold_mode; } UfsParams; /* @@ -186,6 +231,11 @@ typedef struct UfsHc { uint32_t hid_fragment_count; /* Remaining fragmented 4KB units */ uint32_t hid_defrag_total; /* Requested units at defrag start */ uint32_t hid_defrag_remaining; /* Requested units left to move */ + + /* Test and fault injection properties */ + uint32_t active_hold_tag; + UfsHoldMode hold_mode; + UfsRequest *held_req; } UfsHc; static inline uint32_t ufs_mcq_sq_tail(UfsHc *u, uint32_t qid) @@ -309,4 +359,8 @@ void ufs_init_wlu(UfsLu *wlu, uint8_t wlun); bool ufs_realize(UfsHc *u, DeviceState *dev, AddressSpace *dma_as, Error **errp); void ufs_unrealize(UfsHc *u); + +extern const PropertyInfo ufs_prop_hold_tag; +extern const PropertyInfo ufs_prop_hold_mode; + #endif /* HW_UFS_UFS_H */ -- 2.55.0.979.g7e5102b832-goog
