From: Ruslan Ruslichenko <[email protected]> Introduce Fault injection plugin for AArch64 targets. This plugin provides a framework for testing guest OS by injecting hardware-level faults during execution.
The plugin can be configured either statically via an XML file at boot or dynamically at runtime via a UNIX socket. Supported triggers: - PC: Triggers on instruction execution at specific address. - SYS_REG: Intercepts System Registers reads (e.g. mrs) and modifies read results to configured value. - RAM: Triggers on physical memory accesses. - MMIO: Intercepts memory-mapped I/O. - Timer: Triggers at a specific guest virtual clock time (ns). Supported targets (injected faults): - CPU_REG: Corrupts general-purpose CPU registers. - RAM/MMIO: Modifies result of memory reads. - IRQ: Inject hardware irqs on the primary INTC. - EXCP: Inject CPU exceptions (e.g., Serror). - Custom: Triggers device-specific fault handlers registered by device models. Signed-off-by: Ruslan Ruslichenko <[email protected]> --- contrib/plugins/fault_injection.c | 772 ++++++++++++++++++++++++++++++ contrib/plugins/meson.build | 1 + 2 files changed, 773 insertions(+) create mode 100644 contrib/plugins/fault_injection.c diff --git a/contrib/plugins/fault_injection.c b/contrib/plugins/fault_injection.c new file mode 100644 index 0000000000..6fa09fd359 --- /dev/null +++ b/contrib/plugins/fault_injection.c @@ -0,0 +1,772 @@ +/* + * Fault Injection Plugin + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include <ctype.h> +#include <pthread.h> +#include <stdint.h> +#include <inttypes.h> +#include <stdlib.h> +#include <string.h> +#include <sys/param.h> +#include <sys/socket.h> +#include <unistd.h> + +#include "qemu/osdep.h" +#include <qemu-plugin.h> + +#include "glib/gmarkup.h" + +QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION; + +typedef enum { + TRIGGER_ON_PC = 0, + TRIGGER_ON_SYSREG, + TRIGGER_ON_RAM, + TRIGGER_ON_MMIO, + TRIGGER_ON_TIMER +} FaultTrigger; + +typedef enum { + TARGET_EMPTY = 0, + TARGET_CPU_REG, + TARGET_RAM, + TARGET_MMIO, + TARGET_IRQ, + TARGET_EXCP, + TARGET_CUSTOM +} FaultTarget; + +typedef struct { + FaultTarget target; + uint64_t target_data; + + FaultTrigger trigger; + uint64_t trigger_condition; + gchar *trigger_condition_str; + + uint64_t fault_data; + gchar *fault_name; + + uint8_t size; + uint8_t cpu; + gchar *irq_type; +} FaultConfig; + +typedef struct { + uint64_t hwaddr; + uint64_t value; + uint8_t size; +} MmioOverrideConfig; + +#define FI_LOG(...) do { \ + g_autofree gchar *__msg = g_strdup_printf(__VA_ARGS__); \ + qemu_plugin_outs(__msg); \ +} while (0) + +static bool plugin_is_shutting_down = false; +static int socket_fd = -1; + +static GRWLock trigger_lock; + +GHashTable *pc_faults; +GHashTable *mem_faults; +GHashTable *sys_reg_faults; + +static GRWLock mmio_override_lock; +static GRWLock sysreg_lock; + +GHashTable *mmio_override; + +static struct qemu_plugin_register *gp_registers[31]; + +static void register_pc_trigger(FaultConfig* fc); +static void register_mmio_override(FaultConfig *fc); + +static void fc_free(FaultConfig *fc); + +static bool apply_mmio_override(uint64_t hwaddr, unsigned size, bool is_write, + uint64_t *value) +{ + g_rw_lock_reader_lock(&mmio_override_lock); + + MmioOverrideConfig *conf = g_hash_table_lookup(mmio_override, &hwaddr); + if (!conf) { + g_rw_lock_reader_unlock(&mmio_override_lock); + return false; + } + + *value = conf->value; + + g_rw_lock_reader_unlock(&mmio_override_lock); + + return true; +} + +static bool mmio_override_cb(uint64_t hwaddr, unsigned size, bool is_write, + uint64_t *value) +{ + if (is_write) { + return false; + } + + return apply_mmio_override(hwaddr, size, is_write, value); +} + +static void cpu_write_reg(int reg_id, uint64_t value) +{ + g_assert(reg_id >= 0 && reg_id <= 30); + + g_autoptr(GByteArray) buf = g_byte_array_new(); + + g_byte_array_set_size(buf, 8); + + memcpy(buf->data, &value, 8); + + bool success = qemu_plugin_write_register(gp_registers[reg_id], buf); + if (!success) { + FI_LOG("FI: Failed to write register\n"); + } +} + +static void cpu_write_mem(uint64_t addr, uint64_t data, uint8_t size) +{ + g_autoptr(GByteArray) buf = g_byte_array_new(); + + g_byte_array_set_size(buf, size); + + memcpy(buf->data, &data, size); + + bool success = qemu_plugin_write_memory_vaddr(addr, buf); + if (!success) { + FI_LOG("FI: Failed to write memory\n"); + } +} + +static void inject_irq(FaultConfig *fc) +{ + int irq_num = fc->target_data; + + if (!fc->irq_type || !g_strcmp0(fc->irq_type, "SPI")) { + irq_num += 32; + } else if (!g_strcmp0(fc->irq_type, "PPI")) { + irq_num += 16; + } else if (!g_strcmp0(fc->irq_type, "SGI")) { + /* skip */ + } else { + FI_LOG("FI: Unknown IRQ type: %s\n", fc->irq_type); + } + + qemu_plugin_inject_irq(irq_num, fc->cpu, fc->fault_data); + +} + +static void inject_fault(FaultConfig* fc) +{ + switch (fc->target) { + case TARGET_CPU_REG: + cpu_write_reg(fc->target_data, fc->fault_data); + break; + case TARGET_RAM: + cpu_write_mem(fc->target_data, fc->fault_data, fc->size); + break; + case TARGET_MMIO: + register_mmio_override(fc); + break; + case TARGET_IRQ: + inject_irq(fc); + break; + case TARGET_EXCP: + qemu_plugin_inject_exception(fc->target_data, fc->fault_data); + break; + case TARGET_CUSTOM: + qemu_plugin_trigger_custom_fault(fc->fault_name, + &fc->target_data, &fc->fault_data); + break; + default: + FI_LOG("FI: Unsupported fault type\n"); + break; + } +} + +static void timed_fault_timer_cb(void* data) +{ + FaultConfig* fc = (FaultConfig*)data; + + inject_fault(fc); + + fc_free(fc); +} + +static void vcpu_mem_cb(unsigned int vcpu_index, + qemu_plugin_meminfo_t info, + uint64_t vaddr, void *userdata) +{ + GSList *fault_list; + + g_rw_lock_reader_lock(&trigger_lock); + + fault_list = g_hash_table_lookup(mem_faults, &vaddr); + for (GSList *entry = fault_list; entry != NULL; entry = entry->next) { + FaultConfig *fc = (FaultConfig *)entry->data; + + inject_fault(fc); + } + + g_rw_lock_reader_unlock(&trigger_lock); +} + +static void vcpu_insn_exec_cb(unsigned int vcpu_index, void *data) +{ + uint64_t insn_vaddr = (uint64_t)data; + GSList *fault_list; + + g_rw_lock_reader_lock(&trigger_lock); + + fault_list = g_hash_table_lookup(pc_faults, + &insn_vaddr); + + for (GSList *l = fault_list; l != NULL; l = l->next) { + FaultConfig *fc = (FaultConfig *)l->data; + + inject_fault(fc); + } + + g_rw_lock_reader_unlock(&trigger_lock); +} + +#define MRS_OPCODE 0xD5300000 +#define MRS_OPCODE_MASK 0xFFF00000 + +static void handle_sysreg_fault(struct qemu_plugin_insn *insn, uint64_t insn_vaddr) +{ + FaultConfig *fc; + uint32_t raw_opcode; + size_t data_size = qemu_plugin_insn_data(insn, &raw_opcode, sizeof(raw_opcode)); + if (data_size < sizeof(raw_opcode)) { + return; + } + + uint32_t opcode = GUINT32_FROM_LE(raw_opcode); + + if ((opcode & MRS_OPCODE_MASK) != MRS_OPCODE) { + return; + } + + char *disas = qemu_plugin_insn_disas(insn); + if (!disas) { + return; + } + + int dest_reg; + char sysreg_name[32] = { 0 }; + + if (sscanf(disas, "mrs x%d, %31s", &dest_reg, sysreg_name) == 2) { + uint64_t fault_data; + bool found = false; + + g_rw_lock_reader_lock(&sysreg_lock); + + fc = g_hash_table_lookup(sys_reg_faults, sysreg_name); + if (fc) { + fault_data = fc->fault_data; + found = true; + } + + g_rw_lock_reader_unlock(&sysreg_lock); + + if (found) { + /* + * WA: For CPU system registers, injecting fault to destination + * gp register on next PC + */ + FaultConfig *dyn_pc_fault = g_new0(FaultConfig, 1); + + dyn_pc_fault->trigger = TRIGGER_ON_PC; + dyn_pc_fault->trigger_condition = insn_vaddr + 4; + dyn_pc_fault->target = TARGET_CPU_REG; + dyn_pc_fault->target_data = dest_reg; + dyn_pc_fault->fault_data = fault_data; + + register_pc_trigger(dyn_pc_fault); + } + } + + g_free(disas); +} + +static void vcpu_tb_trans_cb(qemu_plugin_id_t id, struct qemu_plugin_tb *tb) +{ + for(int i = 0; i < qemu_plugin_tb_n_insns(tb); i++) { + struct qemu_plugin_insn *insn = qemu_plugin_tb_get_insn(tb, i); + uint64_t insn_vaddr = qemu_plugin_insn_vaddr(insn); + GSList *fault_list; + + qemu_plugin_register_vcpu_mem_cb(insn, vcpu_mem_cb, + QEMU_PLUGIN_CB_NO_REGS, QEMU_PLUGIN_MEM_RW, NULL); + + handle_sysreg_fault(insn, insn_vaddr); + + g_rw_lock_reader_lock(&trigger_lock); + + fault_list = g_hash_table_lookup(pc_faults, + &insn_vaddr); + + if (fault_list) { + qemu_plugin_register_vcpu_insn_exec_cb(insn, vcpu_insn_exec_cb, + QEMU_PLUGIN_CB_RW_REGS, + (void *)insn_vaddr); + } + + g_rw_lock_reader_unlock(&trigger_lock); + } +} + +static void vcpu_init_cb(qemu_plugin_id_t id, unsigned int vcpu_index) +{ + if (vcpu_index) { + /* Init reg's and mem watchpoints only once, with CPU 0 */ + return; + } + + g_autoptr(GArray) reg_list = qemu_plugin_get_registers(); + + for (int i = 0; i < reg_list->len; ++i) { + qemu_plugin_reg_descriptor *rd = &g_array_index(reg_list, + qemu_plugin_reg_descriptor, i); + + if (rd->name[0] == 'x' && isdigit(rd->name[1])) { + int reg_ind = atoi(&rd->name[1]); + + if (reg_ind >= 0 && reg_ind <= 30) { + gp_registers[reg_ind] = rd->handle; + } + } + } +} + +static void register_mmio_override(FaultConfig *fc) +{ + g_rw_lock_writer_lock(&mmio_override_lock); + + MmioOverrideConfig *curr_conf = g_hash_table_lookup(mmio_override, + &fc->target_data); + if (curr_conf) { + curr_conf->value = fc->fault_data; + curr_conf->size = fc->size; + } else { + MmioOverrideConfig *new_conf = g_new0(MmioOverrideConfig, 1); + + new_conf->hwaddr = fc->target_data; + new_conf->value = fc->fault_data; + new_conf->size = fc->size; + + g_hash_table_insert(mmio_override, &new_conf->hwaddr, + new_conf); + } + + g_rw_lock_writer_unlock(&mmio_override_lock); +} + +static void register_sysreg_override(FaultConfig *fc) +{ + g_rw_lock_writer_lock(&sysreg_lock); + + FaultConfig *old_fc = g_hash_table_lookup(sys_reg_faults, + fc->trigger_condition_str); + g_hash_table_replace(sys_reg_faults, + fc->trigger_condition_str, + fc); + + if (old_fc) { + fc_free(old_fc); + } + + g_rw_lock_writer_unlock(&sysreg_lock); +} + +static void register_ram_trigger(FaultConfig* fc) +{ + + g_rw_lock_writer_lock(&trigger_lock); + + GSList *mem_list = g_hash_table_lookup(mem_faults, &fc->trigger_condition); + + mem_list = g_slist_append(mem_list, fc); + g_hash_table_insert(mem_faults, + &fc->trigger_condition, mem_list); + + g_rw_lock_writer_unlock(&trigger_lock); + +} + +static void register_pc_trigger(FaultConfig* fc) +{ + g_rw_lock_writer_lock(&trigger_lock); + + bool duplicate = false; + GSList *pc_list = g_hash_table_lookup(pc_faults, + &fc->trigger_condition); + + for (GSList *l = pc_list; l != NULL; l = l->next) { + FaultConfig *existing = (FaultConfig *)l->data; + + if (existing->target == fc->target && + existing->target_data == fc->target_data && + existing->fault_data == fc->fault_data) { + duplicate = true; + break; + } + } + + if (!duplicate) { + pc_list = g_slist_append(pc_list, fc); + g_hash_table_insert(pc_faults, &fc->trigger_condition, + pc_list); + } else { + fc_free(fc); + } + + g_rw_lock_writer_unlock(&trigger_lock); + +} + +static bool register_fault(FaultConfig *fc) +{ + FaultTrigger trigger_type = fc->trigger; + + if (fc->target == TARGET_CUSTOM && !fc->fault_name) { + FI_LOG("FI: fault_name needed for custom targets\n"); + return false; + } + + if (!fc->size) { + fc->size = sizeof(fc->fault_data); + } + + switch (fc->trigger) { + case TRIGGER_ON_PC: + register_pc_trigger(fc); + break; + case TRIGGER_ON_SYSREG: + if (fc->target != TARGET_EMPTY) { + FI_LOG("FI: SYS_REG faults does not support target\n"); + return false; + } + + register_sysreg_override(fc); + break; + case TRIGGER_ON_RAM: + if (fc->target == TARGET_EMPTY) { + /* Allow short form for RAM triggers to override same memory */ + fc->target = TARGET_RAM; + fc->target_data = fc->trigger_condition; + } + + register_ram_trigger(fc); + break; + case TRIGGER_ON_MMIO: + if (fc->target != TARGET_EMPTY) { + FI_LOG("FI: No target support for MMIO trigger for now\n"); + return false; + } + + register_mmio_override(fc); + fc_free(fc); + break; + case TRIGGER_ON_TIMER: + if (fc->target == TARGET_CPU_REG) { + FI_LOG("FI: CPU_REG is invalid for TIMER trigger\n"); + return false; + } + qemu_plugin_timer_virt_ns(fc->trigger_condition, + timed_fault_timer_cb, fc); + break; + default: + /* skip */ + break; + } + + if (trigger_type == TRIGGER_ON_PC || trigger_type == TRIGGER_ON_SYSREG) { + qemu_plugin_flush_tb_cache(); + } + + return true; +} + +static void fc_free(FaultConfig *fc) +{ + if (!fc) { + return; + } + + g_free(fc->trigger_condition_str); + g_free(fc->fault_name); + g_free(fc->irq_type); + + g_free(fc); +} + +static void xml_start_elem(GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + if (!g_strcmp0(element_name, "Fault")) { + FaultConfig *fc = g_new0(FaultConfig, 1); + + for (int i = 0; attribute_names[i] != NULL; i++) { + const char *key = attribute_names[i]; + const char *value = attribute_values[i]; + + if (!g_strcmp0(key, "target")) { + if (!g_strcmp0(value, "CPU_REG")) { + fc->target = TARGET_CPU_REG; + } else if (!g_strcmp0(value, "RAM")) { + fc->target = TARGET_RAM; + } else if (!g_strcmp0(value, "MMIO")) { + fc->target = TARGET_MMIO; + } else if (!g_strcmp0(value, "IRQ")) { + fc->target = TARGET_IRQ; + } else if (!g_strcmp0(value, "EXCP")) { + fc->target = TARGET_EXCP; + } else if (!g_strcmp0(value, "CUSTOM")) { + fc->target = TARGET_CUSTOM; + } else { + g_set_error(error, G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, + "FI: Unknown target type '%s'", value); + fc_free(fc); + return; + } + } else if (!g_strcmp0(key, "trigger")) { + if (!g_strcmp0(value, "PC")) { + fc->trigger = TRIGGER_ON_PC; + } else if (!g_strcmp0(value, "SYS_REG")) { + fc->trigger = TRIGGER_ON_SYSREG; + } else if (!g_strcmp0(value, "RAM")) { + fc->trigger = TRIGGER_ON_RAM; + } else if (!g_strcmp0(value, "MMIO")) { + fc->trigger = TRIGGER_ON_MMIO; + } else if (!g_strcmp0(value, "TIMER")) { + fc->trigger = TRIGGER_ON_TIMER; + } else { + g_set_error(error, G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, + "FI: Unknown trigger type: '%s'", value); + fc_free(fc); + return; + } + } else if (!g_strcmp0(key, "target_data")) { + fc->target_data = strtoull(value, NULL, 0); + } else if (!g_strcmp0(key, "trigger_condition")) { + fc->trigger_condition_str = g_strdup(value); + fc->trigger_condition = strtoull(value, NULL, 0); + } else if (!g_strcmp0(key, "fault_data")) { + fc->fault_data = strtoull(value, NULL, 0); + } else if (!g_strcmp0(key, "size")) { + fc->size = strtoull(value, NULL, 0); + } else if (!g_strcmp0(key, "cpu")) { + fc->cpu = strtoull(value, NULL, 0); + } else if (!g_strcmp0(key, "irq_type")) { + fc->irq_type = g_strdup(value); + } else if (!g_strcmp0(key, "fault_name")) { + fc->fault_name = g_strdup(value); + } + } + + if (!register_fault(fc)) { + g_set_error(error, G_MARKUP_ERROR, + G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE, + "FI: Failed to register fault"); + fc_free(fc); + return; + } + } +} + +static GMarkupParser parser = { + .start_element = xml_start_elem, +}; + +static void *ipc_listener_thread(void *arg) +{ + char *sock_path = (char *)arg; + struct sockaddr_un addr; + int client_fd; + char buf[1024]; + + socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_fd < 0) { + FI_LOG("Failed to create socket, err = %s\n", + strerror(errno)); + return NULL; + } + + memset(&addr, 0, sizeof(addr)); + + addr.sun_family = AF_UNIX; + g_strlcpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); + + unlink(sock_path); + + if (bind(socket_fd, &addr, sizeof(addr)) < 0) { + FI_LOG("Failed to create socket, err = %s\n", + strerror(errno)); + close(socket_fd); + return NULL; + } + + if (listen(socket_fd, 1)) { + FI_LOG("Listen socket failed, err = %s\n", + strerror(errno)); + close(socket_fd); + return NULL; + } + + while (true) { + client_fd = accept(socket_fd, NULL, NULL); + + if (client_fd < 0) { + if (plugin_is_shutting_down) { + break; + } + continue; + } + + GString *xml_payload = g_string_new(NULL); + + memset(buf, 0, sizeof(buf)); + + while (true) { + ssize_t bytes_read = read(client_fd, buf, sizeof(buf) - 1); + + if (bytes_read > 0) { + g_string_append_len(xml_payload, buf, bytes_read); + } else if (bytes_read == 0) { + break; + } else { + if (errno == EINTR) { + continue; + } + + break; + } + } + + if (xml_payload->len > 0) { + GError *err = NULL; + + GMarkupParseContext *ctx = g_markup_parse_context_new(&parser, + 0, NULL, NULL); + + if (!g_markup_parse_context_parse(ctx, xml_payload->str, + xml_payload->len, &err)) { + FI_LOG("FI Error: Failed to parse dynamic XML: %s\n", + err->message); + g_error_free(err); + } + + g_markup_parse_context_free(ctx); + } + + g_string_free(xml_payload, TRUE); + close(client_fd); + } + + unlink(sock_path); + g_free(sock_path); + + return NULL; +} + +static void plugin_exit_cb(qemu_plugin_id_t id, void *userdata) +{ + plugin_is_shutting_down = true; + + if (socket_fd >= 0) { + close(socket_fd); + socket_fd = -1; + } +} + +QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id, + const qemu_info_t *info, + int argc, char **argv) +{ + const char *config_path = NULL; + const char *socket_path = NULL; + gchar *config; + gsize length; + GError *err = NULL; + bool success; + + if (strcmp(info->target_name, "aarch64")) { + FI_LOG("FI: Target %s is not supported\n", info->target_name); + return 1; + } + + for (int i = 0; i < argc; ++i) { + if (g_str_has_prefix(argv[i], "config=")) { + config_path = argv[i] + strlen("config="); + } else if (g_str_has_prefix(argv[i], "socket=")) { + socket_path = g_strdup(argv[i] + strlen("socket=")); + } + } + + if (!config_path && !socket_path) { + FI_LOG("FI: either config or socket path required\n"); + return 1; + } + + pc_faults = g_hash_table_new(g_int64_hash, g_int64_equal); + mem_faults = g_hash_table_new(g_int64_hash, g_int64_equal); + sys_reg_faults = g_hash_table_new(g_str_hash, g_str_equal); + mmio_override = g_hash_table_new(g_int64_hash, g_int64_equal); + + g_rw_lock_init(&trigger_lock); + g_rw_lock_init(&mmio_override_lock); + g_rw_lock_init(&sysreg_lock); + + if (config_path) { + if (access(config_path, R_OK)) { + FI_LOG("FI: can't access config file, err = %s\n", + strerror(errno)); + return 1; + } + + success = g_file_get_contents(config_path, &config, + &length, &err); + if (success) { + GMarkupParseContext *ctx = g_markup_parse_context_new(&parser, + 0, NULL, NULL); + + success = g_markup_parse_context_parse(ctx, config, length, &err); + } + + if (!success) { + FI_LOG("FI: failed to parse config file\n"); + return 1; + } + } + + if (socket_path) { + pthread_t thread_id; + + pthread_create(&thread_id, NULL, ipc_listener_thread, + (void*)socket_path); + pthread_detach(thread_id); + } + + qemu_plugin_register_vcpu_init_cb(id, vcpu_init_cb); + qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans_cb); + qemu_plugin_register_mmio_override_cb(id, mmio_override_cb); + + qemu_plugin_register_atexit_cb(id, plugin_exit_cb, NULL); + + return 0; +} \ No newline at end of file diff --git a/contrib/plugins/meson.build b/contrib/plugins/meson.build index 099319e7a1..df4d4c5177 100644 --- a/contrib/plugins/meson.build +++ b/contrib/plugins/meson.build @@ -12,6 +12,7 @@ contrib_plugins = [ 'stoptrigger.c', 'traps.c', 'uftrace.c', +'fault_injection.c', ] if host_os != 'windows' -- 2.43.0
