故障注入功能基础设施¶
另请参阅 drivers/md/md-faulty.c 和 scsi_debug 的“every_nth”模块选项。
可用的故障注入功能¶
failslab
注入 slab 分配失败。(
kmalloc()
,kmem_cache_alloc()
, ...)fail_page_alloc
注入页面分配失败。(
alloc_pages()
, get_free_pages(), ...)fail_usercopy
在用户内存访问函数中注入失败。(copy_from_user(),
get_user()
, ...)fail_futex
注入 futex 死锁和 uaddr 故障错误。
fail_sunrpc
注入内核 RPC 客户端和服务器故障。
fail_make_request
在通过设置 /sys/block/<device>/make-it-fail 或 /sys/block/<device>/<partition>/make-it-fail 允许的设备上注入磁盘 IO 错误。(
submit_bio_noacct()
)fail_mmc_request
在 /sys/kernel/debug/mmc0/fail_mmc_request 下通过设置 debugfs 条目允许的设备上注入 MMC 数据错误。
fail_function
通过设置 /sys/kernel/debug/fail_function 下的 debugfs 条目,在由 ALLOW_ERROR_INJECTION() 宏标记的特定函数上注入错误返回。不支持启动选项。
fail_skb_realloc
将 skb(套接字缓冲区)重新分配事件注入到网络路径中。主要目标是识别和防止与网络子系统中的指针管理不当相关的问题。通过在战略点强制 skb 重新分配,此功能会创建现有 skb 标头的指针变为无效的场景。
当注入故障并触发重新分配时,缓存的 skb 标头和数据指针不再引用有效的内存位置。这种故意的失效有助于暴露在重新分配事件后忽略正确指针更新的代码路径。
通过创建这些受控的故障场景,系统可以捕获使用过时指针的实例,这可能会导致内存损坏或系统不稳定。
要选择要操作的接口,请将网络名称写入 /sys/kernel/debug/fail_skb_realloc/devname。如果此字段为空(默认值),则将在所有网络接口上强制执行 skb 重新分配。
当启用 KASAN 时,此故障检测的有效性会得到增强,因为它有助于识别无效的内存引用和释放后使用 (UAF) 问题。
NVMe 故障注入
在通过设置 /sys/kernel/debug/nvme*/fault_inject 下的 debugfs 条目允许的设备上注入 NVMe 状态代码和重试标志。默认状态代码为 NVME_SC_INVALID_OPCODE,不重试。可以通过 debugfs 设置状态代码和重试标志。
空测试块驱动程序故障注入
通过设置 /sys/kernel/config/nullb/<disk>/timeout_inject 下的配置项注入 IO 超时,通过设置 /sys/kernel/config/nullb/<disk>/requeue_inject 下的配置项注入重新排队请求,并通过设置 /sys/kernel/config/nullb/<disk>/init_hctx_fault_inject 下的配置项注入 init_hctx() 错误。
配置故障注入功能行为¶
debugfs 条目¶
fault-inject-debugfs 内核模块为故障注入功能的运行时配置提供了一些 debugfs 条目。
/sys/kernel/debug/fail*/probability
故障注入的可能性,以百分比表示。
格式:<百分比>
请注意,对于某些测试用例,百分之一的故障率是非常高的错误率。考虑设置 probability=100 并为这些测试用例配置 /sys/kernel/debug/fail*/interval。
/sys/kernel/debug/fail*/interval
指定故障之间的间隔,对于通过所有其他测试的 should_fail() 调用。
请注意,如果启用此选项,通过设置 interval>1,您可能需要设置 probability=100。
/sys/kernel/debug/fail*/times
指定最多可能发生故障的次数。值为 -1 表示“无限制”。
/sys/kernel/debug/fail*/space
指定初始资源“预算”,在每次调用 should_fail(,size) 时按“size”递减。在“space”达到零之前,抑制故障注入。
/sys/kernel/debug/fail*/verbose
格式:{ 0 | 1 | 2 }
指定注入故障时消息的详细程度。“0”表示没有消息;“1”将为每个故障仅打印一个日志行;“2”也将打印调用跟踪 - 用于调试故障注入暴露的问题。
/sys/kernel/debug/fail*/task-filter
格式:{ ‘Y’ | ‘N’ }
值为“N”表示禁用按进程筛选(默认)。任何正值都将故障限制为仅由 /proc/<pid>/make-it-fail==1 指示的进程。
/sys/kernel/debug/fail*/require-start, /sys/kernel/debug/fail*/require-end, /sys/kernel/debug/fail*/reject-start, /sys/kernel/debug/fail*/reject-end
指定在堆栈跟踪期间测试的虚拟地址范围。只有在遍历的堆栈跟踪中的某个调用方位于所需范围内,并且没有调用方位于拒绝范围内时才注入故障。默认所需范围是 [0,ULONG_MAX) (整个虚拟地址空间)。默认拒绝范围是 [0,0)。
/sys/kernel/debug/fail*/stacktrace-depth
指定在 [require-start,require-end) 或 [reject-start,reject-end) 中搜索调用方时遍历的最大堆栈跟踪深度。
/sys/kernel/debug/fail_page_alloc/ignore-gfp-highmem
格式:{ ‘Y’ | ‘N’ }
默认为 “Y”,将其设置为 “N” 也会将故障注入到 highmem/用户分配(__GFP_HIGHMEM 分配)中。
- /sys/kernel/debug/failslab/cache-filter
格式:{ ‘Y’ | ‘N’ }
默认为 “N”,将其设置为 “Y” 仅在从某些缓存请求对象时注入故障。
通过将 “1” 写入 /sys/kernel/slab/<cache>/failslab 来选择缓存
/sys/kernel/debug/failslab/ignore-gfp-wait
/sys/kernel/debug/fail_page_alloc/ignore-gfp-wait
格式:{ ‘Y’ | ‘N’ }
默认为 “Y”,将其设置为 “N” 也会将故障注入到可以休眠的分配(__GFP_DIRECT_RECLAIM 分配)中。
/sys/kernel/debug/fail_page_alloc/min-order
指定要注入故障的最小页面分配顺序。
/sys/kernel/debug/fail_futex/ignore-private
格式:{ ‘Y’ | ‘N’ }
默认为 “N”,将其设置为 “Y” 将在处理私有(地址空间)futex 时禁用故障注入。
/sys/kernel/debug/fail_sunrpc/ignore-client-disconnect
格式:{ ‘Y’ | ‘N’ }
默认为 “N”,将其设置为 “Y” 将禁用 RPC 客户端上的断开连接注入。
/sys/kernel/debug/fail_sunrpc/ignore-server-disconnect
格式:{ ‘Y’ | ‘N’ }
默认为 “N”,将其设置为 “Y” 将禁用 RPC 服务器上的断开连接注入。
/sys/kernel/debug/fail_sunrpc/ignore-cache-wait
格式:{ ‘Y’ | ‘N’ }
默认为 “N”,将其设置为 “Y” 将禁用 RPC 服务器上的缓存等待注入。
/sys/kernel/debug/fail_function/inject
格式:{ ‘function-name’ | ‘!function-name’ | ‘’ }
按名称指定错误注入的目标函数。如果函数名称带有“!”前缀,则从注入列表中删除给定函数。如果未指定任何内容(‘’),则清除注入列表。
/sys/kernel/debug/fail_function/injectable
(只读)显示可注入错误的函数以及可以指定哪些类型的错误值。错误类型将是以下之一;- NULL:retval 必须为 0。 - ERRNO:retval 必须为 -1 到 -MAX_ERRNO (-4096)。 - ERR_NULL:retval 必须为 0 或 -1 到 -MAX_ERRNO (-4096)。
/sys/kernel/debug/fail_function/<function-name>/retval
指定要注入到给定函数的“错误”返回值。当用户指定新的注入条目时,将创建此值。请注意,此文件仅接受无符号值。因此,如果要使用负 errno,最好使用 “printf” 而不是 “echo”,例如:$ printf %#x -12 > retval
/sys/kernel/debug/fail_skb_realloc/devname
指定要在其上强制执行 SKB 重新分配的网络接口。如果留空,则 SKB 重新分配将应用于所有网络接口。
用法示例
# Force skb reallocation on eth0 echo "eth0" > /sys/kernel/debug/fail_skb_realloc/devname # Clear the selection and force skb reallocation on all interfaces echo "" > /sys/kernel/debug/fail_skb_realloc/devname
启动选项¶
为了在 debugfs 不可用时(早期启动时间)注入故障,请使用启动选项
failslab=
fail_page_alloc=
fail_usercopy=
fail_make_request=
fail_futex=
fail_skb_realloc=
mmc_core.fail_request=<interval>,<probability>,<space>,<times>
proc 条目¶
/proc/<pid>/fail-nth, /proc/self/task/<tid>/fail-nth
将整数 N 写入此文件会使任务中的第 N 次调用失败。从此文件读取会返回一个整数值。值为“0”表示使用先前写入此文件的故障设置已注入。正整数 N 表示尚未注入故障。请注意,此文件启用所有类型的故障(slab、futex 等)。此设置优先于所有其他通用 debugfs 设置,如 probability、interval、times 等。但是每个功能设置(例如 fail_futex/ignore-private)优先于它。
此功能旨在对单个系统调用中的故障进行系统测试。请参见下面的示例。
错误可注入函数¶
此部分适用于考虑向 ALLOW_ERROR_INJECTION() 宏添加函数的内核开发人员。
错误可注入函数的要求¶
由于函数级别的错误注入会强制更改代码路径并返回错误,即使输入和条件正确,如果您允许在 NOT 错误可注入的函数上进行错误注入,这可能会导致意外的内核崩溃。因此,您(和审查人员)必须确保;
如果函数失败,则返回错误代码,调用者必须正确检查错误代码(需要从中恢复)。
函数在第一次返回错误之前,不会执行任何可以更改任何状态的代码。状态包括全局或局部变量,或输入变量。例如,清除输出地址存储(例如 *ret = NULL),递增/递减计数器,设置标志,抢占/中断禁用或获取锁(如果在返回错误之前恢复了这些操作,则可以)。
第一条要求很重要,它会导致释放(释放对象)函数通常比分配函数更难注入错误。如果此类释放函数的错误未得到正确处理,则很容易导致内存泄漏(调用者会混淆对象是否已被释放或已损坏)。
第二条要求是针对期望函数始终执行某些操作的调用者。因此,如果函数错误注入跳过了整个函数,则会背叛期望并导致意外错误。
可注入错误函数的类型¶
每个可注入错误函数都将具有由 ALLOW_ERROR_INJECTION() 宏指定的错误类型。如果添加新的可注入错误函数,则必须仔细选择它。如果选择了错误的错误类型,内核可能会崩溃,因为它可能无法处理该错误。include/asm-generic/error-injection.h 中定义了 4 种错误类型。
- EI_ETYPE_NULL
如果此函数失败,它将返回 NULL。例如,返回分配的对象地址。
- EI_ETYPE_ERRNO
如果此函数失败,它将返回一个 -errno 错误代码。例如,如果输入错误,则返回 -EINVAL。这包括通过
ERR_PTR()
宏返回编码 -errno 的地址的函数。- EI_ETYPE_ERRNO_NULL
如果此函数失败,它将返回一个 -errno 或 NULL。如果此函数的调用者使用
IS_ERR_OR_NULL()
宏检查返回值,则此类型将适用。- EI_ETYPE_TRUE
如果此函数失败,它将返回 true(非零正值)。
如果指定错误的类型,例如,对于返回分配的对象的函数使用 EI_TYPE_ERRNO,则可能会导致问题,因为返回的值不是对象地址,并且调用者无法访问该地址。
如何添加新的故障注入功能¶
#include <linux/fault-inject.h>
定义故障属性
DECLARE_FAULT_ATTR(name);
有关详细信息,请参阅 fault-inject.h 中 struct fault_attr 的定义。
提供一种配置故障属性的方法
启动选项
如果需要从启动时启用故障注入功能,可以提供启动选项来配置它。有一个辅助函数可以实现此目的
setup_fault_attr(attr, str);
debugfs 条目
failslab、fail_page_alloc、fail_usercopy 和 fail_make_request 使用这种方式。辅助函数
fault_create_debugfs_attr(name, parent, attr);
模块参数
如果故障注入功能的范围仅限于单个内核模块,则最好提供模块参数来配置故障属性。
添加一个挂钩以插入故障
当 should_fail() 返回 true 时,客户端代码应注入故障
should_fail(attr, size);
应用示例¶
将 slab 分配故障注入到模块初始化/退出代码中
#!/bin/bash FAILTYPE=failslab echo Y > /sys/kernel/debug/$FAILTYPE/task-filter echo 10 > /sys/kernel/debug/$FAILTYPE/probability echo 100 > /sys/kernel/debug/$FAILTYPE/interval echo -1 > /sys/kernel/debug/$FAILTYPE/times echo 0 > /sys/kernel/debug/$FAILTYPE/space echo 2 > /sys/kernel/debug/$FAILTYPE/verbose echo Y > /sys/kernel/debug/$FAILTYPE/ignore-gfp-wait faulty_system() { bash -c "echo 1 > /proc/self/make-it-fail && exec $*" } if [ $# -eq 0 ] then echo "Usage: $0 modulename [ modulename ... ]" exit 1 fi for m in $* do echo inserting $m... faulty_system modprobe $m echo removing $m... faulty_system modprobe -r $m done
仅针对特定模块注入页面分配故障
#!/bin/bash FAILTYPE=fail_page_alloc module=$1 if [ -z $module ] then echo "Usage: $0 <modulename>" exit 1 fi modprobe $module if [ ! -d /sys/module/$module/sections ] then echo Module $module is not loaded exit 1 fi cat /sys/module/$module/sections/.text > /sys/kernel/debug/$FAILTYPE/require-start cat /sys/module/$module/sections/.data > /sys/kernel/debug/$FAILTYPE/require-end echo N > /sys/kernel/debug/$FAILTYPE/task-filter echo 10 > /sys/kernel/debug/$FAILTYPE/probability echo 100 > /sys/kernel/debug/$FAILTYPE/interval echo -1 > /sys/kernel/debug/$FAILTYPE/times echo 0 > /sys/kernel/debug/$FAILTYPE/space echo 2 > /sys/kernel/debug/$FAILTYPE/verbose echo Y > /sys/kernel/debug/$FAILTYPE/ignore-gfp-wait echo Y > /sys/kernel/debug/$FAILTYPE/ignore-gfp-highmem echo 10 > /sys/kernel/debug/$FAILTYPE/stacktrace-depth trap "echo 0 > /sys/kernel/debug/$FAILTYPE/probability" SIGINT SIGTERM EXIT echo "Injecting errors into the module $module... (interrupt to stop)" sleep 1000000
在 btrfs 挂载时注入 open_ctree 错误
#!/bin/bash rm -f testfile.img dd if=/dev/zero of=testfile.img bs=1M seek=1000 count=1 DEVICE=$(losetup --show -f testfile.img) mkfs.btrfs -f $DEVICE mkdir -p tmpmnt FAILTYPE=fail_function FAILFUNC=open_ctree echo $FAILFUNC > /sys/kernel/debug/$FAILTYPE/inject printf %#x -12 > /sys/kernel/debug/$FAILTYPE/$FAILFUNC/retval echo N > /sys/kernel/debug/$FAILTYPE/task-filter echo 100 > /sys/kernel/debug/$FAILTYPE/probability echo 0 > /sys/kernel/debug/$FAILTYPE/interval echo -1 > /sys/kernel/debug/$FAILTYPE/times echo 0 > /sys/kernel/debug/$FAILTYPE/space echo 1 > /sys/kernel/debug/$FAILTYPE/verbose mount -t btrfs $DEVICE tmpmnt if [ $? -ne 0 ] then echo "SUCCESS!" else echo "FAILED!" umount tmpmnt fi echo > /sys/kernel/debug/$FAILTYPE/inject rmdir tmpmnt losetup -d $DEVICE rm testfile.img
仅注入 skbuff 分配故障
# mark skbuff_head_cache as faulty echo 1 > /sys/kernel/slab/skbuff_head_cache/failslab # Turn on cache filter (off by default) echo 1 > /sys/kernel/debug/failslab/cache-filter # Turn on fault injection echo 1 > /sys/kernel/debug/failslab/times echo 1 > /sys/kernel/debug/failslab/probability
使用 failslab 或 fail_page_alloc 运行命令的工具¶
为了更容易地完成上述任务,我们可以使用 tools/testing/fault-injection/failcmd.sh。请运行命令“./tools/testing/fault-injection/failcmd.sh --help”以获取更多信息,并查看以下示例。
示例
运行命令“make -C tools/testing/selftests/ run_tests”并注入 slab 分配故障
# ./tools/testing/fault-injection/failcmd.sh \
-- make -C tools/testing/selftests/ run_tests
与上面相同,但指定最多 100 次故障,而不是默认最多一次
# ./tools/testing/fault-injection/failcmd.sh --times=100 \
-- make -C tools/testing/selftests/ run_tests
与上面相同,但注入页面分配故障而不是 slab 分配故障
# env FAILCMD_TYPE=fail_page_alloc \
./tools/testing/fault-injection/failcmd.sh --times=100 \
-- make -C tools/testing/selftests/ run_tests
使用 fail-nth 的系统性故障¶
以下代码系统性地在 socketpair() 系统调用中产生第 0 个、第 1 个、第 2 个等功能故障
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
int main()
{
int i, err, res, fail_nth, fds[2];
char buf[128];
system("echo N > /sys/kernel/debug/failslab/ignore-gfp-wait");
sprintf(buf, "/proc/self/task/%ld/fail-nth", syscall(SYS_gettid));
fail_nth = open(buf, O_RDWR);
for (i = 1;; i++) {
sprintf(buf, "%d", i);
write(fail_nth, buf, strlen(buf));
res = socketpair(AF_LOCAL, SOCK_STREAM, 0, fds);
err = errno;
pread(fail_nth, buf, sizeof(buf), 0);
if (res == 0) {
close(fds[0]);
close(fds[1]);
}
printf("%d-th fault %c: res=%d/%d\n", i, atoi(buf) ? 'N' : 'Y',
res, err);
if (atoi(buf))
break;
}
return 0;
}
一个示例输出
1-th fault Y: res=-1/23
2-th fault Y: res=-1/23
3-th fault Y: res=-1/12
4-th fault Y: res=-1/12
5-th fault Y: res=-1/23
6-th fault Y: res=-1/23
7-th fault Y: res=-1/23
8-th fault Y: res=-1/12
9-th fault Y: res=-1/12
10-th fault Y: res=-1/12
11-th fault Y: res=-1/12
12-th fault Y: res=-1/12
13-th fault Y: res=-1/12
14-th fault Y: res=-1/12
15-th fault Y: res=-1/12
16-th fault N: res=0/12