故障注入能力基础设施

另请参阅 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

    在特定函数上注入错误返回,这些函数通过 ALLOW_ERROR_INJECTION() 宏标记,通过设置 /sys/kernel/debug/fail_function 下的 debugfs 条目。 不支持引导选项。

  • 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 设置状态代码和重试标志。

  • Null test 块驱动程序故障注入

    通过设置 /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

    故障注入的可能性,以百分比表示。

    格式:<percent>

    请注意,对于某些测试用例,百分之一的失败率是一个非常高的错误率。 考虑设置 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) OR [reject-start,reject-end) 中搜索调用者期间行走的堆栈跟踪最大深度。

  • /sys/kernel/debug/fail_page_alloc/ignore-gfp-highmem

    格式:{ ‘Y’ | ‘N’ }

    默认为“Y”,将其设置为“N”也会将失败注入到 highmem/user 分配中(__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”将在处理私有(地址空间)futexes 时禁用失败注入。

  • /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 设置,例如概率、间隔、次数等。但是,每个功能的设置(例如 fail_futex/ignore-private)优先于它。

    此功能旨在用于系统地测试单个系统调用中的故障。 请参见下面的示例。

可注入错误的函数

此部分适用于考虑将函数添加到 ALLOW_ERROR_INJECTION() 宏的内核开发人员。

可注入错误函数的要求

由于函数级别的错误注入强制更改代码路径,即使输入和条件正确也会返回错误,如果在 NOT 错误可注入的函数上允许错误注入,这可能会导致意外的内核崩溃。 因此,您(和审核者)必须确保;

  • 如果函数失败,则返回错误代码,并且调用者必须正确检查它(需要从中恢复)。

  • 该函数在第一次错误返回之前不会执行任何可以更改任何状态的代码。 状态包括全局或本地状态,或输入变量。 例如,清除输出地址存储(例如*ret = NULL),递增/递减计数器,设置标志,抢占/irq 禁用或获取锁(如果在返回错误之前恢复了这些,那将是可以的。)

第一个要求很重要,它会导致释放(释放对象)函数通常比分配函数更难注入错误。 如果未正确处理此类释放函数的错误,则容易导致内存泄漏(调用者会混淆对象已被释放或损坏。)

第二个是针对期望该函数始终执行某些操作的调用者。 因此,如果函数错误注入跳过整个函数,则期望会落空,并导致意外的错误。

可注入错误函数的类型

每个可注入错误的函数都将具有 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

如果此函数失败,将返回 -errnoNULL。 如果此函数的调用者使用 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 分配失败注入到模块 init/exit 代码中

    #!/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”以获取更多信息,并查看以下示例。

示例

通过注入 slab 分配失败来运行命令“make -C tools/testing/selftests/ run_tests”

# ./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