// 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);}
// 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);
}