| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Copyright (c) 2026 Christoph Hellwig. | |
| */ | |
| #include <linux/debugfs.h> | |
| #include <linux/blkdev.h> | |
| #include <linux/parser.h> | |
| #include <linux/seq_file.h> | |
| #include "blk.h" | |
| #include "error-injection.h" | |
| struct blk_error_inject { | |
| struct list_head entry; | |
| sector_t start; | |
| sector_t end; | |
| enum req_op op; | |
| blk_status_t status; | |
| /* only inject every 1 / chance times */ | |
| unsigned int chance; | |
| }; | |
| DEFINE_STATIC_KEY_FALSE(blk_error_injection_enabled); | |
| bool __blk_error_inject(struct bio *bio) | |
| { | |
| struct gendisk *disk = bio->bi_bdev->bd_disk; | |
| struct blk_error_inject *inj; | |
| rcu_read_lock(); | |
| list_for_each_entry_rcu(inj, &disk->error_injection_list, entry) { | |
| if (bio_op(bio) != inj->op) | |
| continue; | |
| /* | |
| * This never matches 0-sized bios like empty WRITEs with | |
| * REQ_PREFLUSH or ZONE_RESET_ALL. While adding a special case | |
| * for them would be trivial, that means any WRITE rule would | |
| * trigger for flushes. So before we can make this work | |
| * properly, we'll need to start using REQ_OP_FLUSH for pure | |
| * flushes at the bio level like we already do in blk-mq. | |
| */ | |
| if (bio->bi_iter.bi_sector > inj->end || | |
| bio_end_sector(bio) <= inj->start) | |
| continue; | |
| if (inj->chance > 1 && (get_random_u32() % inj->chance) != 0) | |
| continue; | |
| pr_info_ratelimited("%pg: injecting %s error for %s at sector %llu:%u\n", | |
| disk->part0, blk_status_to_str(inj->status), | |
| blk_op_str(inj->op), bio->bi_iter.bi_sector, | |
| bio_sectors(bio)); | |
| bio->bi_status = inj->status; | |
| rcu_read_unlock(); | |
| bio_endio(bio); | |
| return true; | |
| } | |
| rcu_read_unlock(); | |
| return false; | |
| } | |
| static int error_inject_add(struct gendisk *disk, enum req_op op, | |
| sector_t start, u64 nr_sectors, blk_status_t status, | |
| unsigned int chance) | |
| { | |
| struct blk_error_inject *inj; | |
| int error = -EINVAL; | |
| if (op == REQ_OP_LAST) | |
| return -EINVAL; | |
| if (status == BLK_STS_OK) | |
| return -EINVAL; | |
| inj = kzalloc_obj(*inj); | |
| if (!inj) | |
| return -ENOMEM; | |
| if (nr_sectors) { | |
| if (U64_MAX - nr_sectors < start) | |
| goto out_free_inj; | |
| inj->end = start + nr_sectors - 1; | |
| } else { | |
| inj->end = U64_MAX; | |
| } | |
| inj->op = op; | |
| inj->start = start; | |
| inj->status = status; | |
| inj->chance = chance; | |
| pr_debug_ratelimited("%pg: adding %s injection for %s at sector %llu:%llu\n", | |
| disk->part0, blk_status_to_str(status), | |
| blk_op_str(op), | |
| start, nr_sectors); | |
| /* | |
| * Add to the front of the list so that newer entries can partially | |
| * override other entries. This also intentionally allows duplicate | |
| * entries as there is no real reason to reject them. | |
| */ | |
| mutex_lock(&disk->error_injection_lock); | |
| if (!disk_live(disk)) { | |
| mutex_unlock(&disk->error_injection_lock); | |
| error = -ENODEV; | |
| goto out_free_inj; | |
| } | |
| if (list_empty(&disk->error_injection_list)) | |
| static_branch_inc(&blk_error_injection_enabled); | |
| list_add_rcu(&inj->entry, &disk->error_injection_list); | |
| set_bit(GD_ERROR_INJECT, &disk->state); | |
| mutex_unlock(&disk->error_injection_lock); | |
| return 0; | |
| out_free_inj: | |
| kfree(inj); | |
| return error; | |
| } | |
| static void error_inject_removeall(struct gendisk *disk) | |
| { | |
| struct blk_error_inject *inj; | |
| mutex_lock(&disk->error_injection_lock); | |
| if (test_and_clear_bit(GD_ERROR_INJECT, &disk->state)) | |
| static_branch_dec(&blk_error_injection_enabled); | |
| while ((inj = list_first_entry_or_null(&disk->error_injection_list, | |
| struct blk_error_inject, entry))) { | |
| list_del_rcu(&inj->entry); | |
| kfree_rcu_mightsleep(inj); | |
| } | |
| mutex_unlock(&disk->error_injection_lock); | |
| } | |
| enum options { | |
| Opt_add = (1u << 0), | |
| Opt_removeall = (1u << 1), | |
| Opt_op = (1u << 16), | |
| Opt_start = (1u << 17), | |
| Opt_nr_sectors = (1u << 18), | |
| Opt_status = (1u << 19), | |
| Opt_chance = (1u << 20), | |
| Opt_invalid, | |
| }; | |
| static const match_table_t opt_tokens = { | |
| { Opt_add, "add", }, | |
| { Opt_removeall, "removeall", }, | |
| { Opt_op, "op=%s", }, | |
| { Opt_start, "start=%u" }, | |
| { Opt_nr_sectors, "nr_sectors=%u" }, | |
| { Opt_status, "status=%s" }, | |
| { Opt_chance, "chance=%u" }, | |
| { Opt_invalid, NULL, }, | |
| }; | |
| static int match_op(substring_t *args, enum req_op *op) | |
| { | |
| const char *tag; | |
| tag = match_strdup(args); | |
| if (!tag) | |
| return -ENOMEM; | |
| *op = str_to_blk_op(tag); | |
| if (*op == REQ_OP_LAST) | |
| pr_warn("invalid op '%s'\n", tag); | |
| kfree(tag); | |
| return 0; | |
| } | |
| static int match_status(substring_t *args, blk_status_t *status) | |
| { | |
| const char *tag; | |
| tag = match_strdup(args); | |
| if (!tag) | |
| return -ENOMEM; | |
| *status = tag_to_blk_status(tag); | |
| if (!*status) | |
| pr_warn("invalid status '%s'\n", tag); | |
| kfree(tag); | |
| return 0; | |
| } | |
| static ssize_t blk_error_injection_parse_options(struct gendisk *disk, | |
| char *options) | |
| { | |
| enum { Unset, Add, Removeall } action = Unset; | |
| unsigned int option_mask = 0, chance = 1; | |
| enum req_op op = REQ_OP_LAST; | |
| u64 start = 0, nr_sectors = 0; | |
| blk_status_t status = BLK_STS_OK; | |
| substring_t args[MAX_OPT_ARGS]; | |
| char *p; | |
| while ((p = strsep(&options, ",\n")) != NULL) { | |
| int error = 0; | |
| ssize_t token; | |
| if (!*p) | |
| continue; | |
| token = match_token(p, opt_tokens, args); | |
| option_mask |= token; | |
| switch (token) { | |
| case Opt_add: | |
| if (action != Unset) | |
| return -EINVAL; | |
| action = Add; | |
| break; | |
| case Opt_removeall: | |
| if (action != Unset) | |
| return -EINVAL; | |
| action = Removeall; | |
| break; | |
| case Opt_op: | |
| error = match_op(args, &op); | |
| break; | |
| case Opt_start: | |
| error = match_u64(args, &start); | |
| break; | |
| case Opt_nr_sectors: | |
| error = match_u64(args, &nr_sectors); | |
| break; | |
| case Opt_status: | |
| error = match_status(args, &status); | |
| break; | |
| case Opt_chance: | |
| error = match_uint(args, &chance); | |
| if (!error && chance == 0) | |
| error = -EINVAL; | |
| break; | |
| default: | |
| pr_warn("unknown parameter or missing value '%s'\n", p); | |
| error = -EINVAL; | |
| } | |
| if (error) | |
| return error; | |
| } | |
| switch (action) { | |
| case Add: | |
| return error_inject_add(disk, op, start, nr_sectors, status, | |
| chance); | |
| case Removeall: | |
| if (option_mask & ~Opt_removeall) | |
| return -EINVAL; | |
| error_inject_removeall(disk); | |
| return 0; | |
| default: | |
| return -EINVAL; | |
| } | |
| } | |
| static ssize_t blk_error_injection_write(struct file *file, | |
| const char __user *ubuf, size_t count, loff_t *pos) | |
| { | |
| struct gendisk *disk = file_inode(file)->i_private; | |
| char *options; | |
| int error; | |
| options = memdup_user_nul(ubuf, count); | |
| if (IS_ERR(options)) | |
| return PTR_ERR(options); | |
| error = blk_error_injection_parse_options(disk, options); | |
| kfree(options); | |
| if (error) | |
| return error; | |
| return count; | |
| } | |
| static int blk_error_injection_show(struct seq_file *s, void *private) | |
| { | |
| struct gendisk *disk = s->private; | |
| struct blk_error_inject *inj; | |
| rcu_read_lock(); | |
| list_for_each_entry_rcu(inj, &disk->error_injection_list, entry) { | |
| seq_printf(s, "%llu:%llu op=%s,status=%s,chance=%u", | |
| inj->start, inj->end, | |
| blk_op_str(inj->op), | |
| blk_status_to_tag(inj->status), inj->chance); | |
| seq_putc(s, '\n'); | |
| } | |
| rcu_read_unlock(); | |
| return 0; | |
| } | |
| static int blk_error_injection_open(struct inode *inode, struct file *file) | |
| { | |
| return single_open(file, blk_error_injection_show, inode->i_private); | |
| } | |
| static int blk_error_injection_release(struct inode *inode, struct file *file) | |
| { | |
| return single_release(inode, file); | |
| } | |
| static const struct file_operations blk_error_injection_fops = { | |
| .owner = THIS_MODULE, | |
| .write = blk_error_injection_write, | |
| .read = seq_read, | |
| .open = blk_error_injection_open, | |
| .release = blk_error_injection_release, | |
| }; | |
| void blk_error_injection_init(struct gendisk *disk) | |
| { | |
| debugfs_create_file("error_injection", 0600, disk->queue->debugfs_dir, | |
| disk, &blk_error_injection_fops); | |
| } | |
| void blk_error_injection_exit(struct gendisk *disk) | |
| { | |
| error_inject_removeall(disk); | |
| } |