316 lines · 7.4 KB · C
RawDownload
// 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);}