捕获内核的内存污染问题(KFENCE)

KFENCE(Kernel Electric-Fence)是Linux内核内置的一项工具,可在在线环境中启用,旨在捕获内核及内核模块的内存污染问题。当检测到内存污染问题时,KFENCE会触发错误报告,并提供关于该问题的详细信息。阿里云在Alibaba Cloud Linux 3中对KFENCE功能进行了增强,支持灵活的动态开关KFENCE以及全面捕获内存污染问题,从而兼顾了线上探测与线下调试的需求。

操作系统限制

  • x86架构

    Alibaba Cloud Linux 3(内核5.10.84-10及以上版本)

  • ARM架构

    Alibaba Cloud Linux 3(内核5.10.134-16及以上版本)

说明
  • 如果您是一名内核或内核模块的开发者,可以用此工具来检查您开发的内核或内核模块是否存在内存污染问题。

  • 如果您是一名普通用户但遇到了内核崩溃的问题,可以打开KFENCE帮助我们或第三方驱动模块的开发者收集更多信息。

基本概念

名词

说明

内存污染

指在程序运行过程中,内存区域被错误地修改或破坏,导致程序行为异常或崩溃的问题。内存污染可能是由于编程错误、软件漏洞、恶意软件或硬件故障等原因引起的。

slab

slab是Linux内核中一种高效的内存分配机制。它通过预先分配一定数量的内存对象,组织成一个内存缓存池,用于快速分配和释放内存。slab可以避免频繁的内存分配和释放操作,提高内存分配的效率。

order 0单页

order 0单页是Linux内核中一种内存分配机制,内存被分割成固定大小的页框(page frame),一般为4 KiB。order 0的单页指的就是一个普通的4 KiB大小的内存页框,它是内存分配的基本单位。当应用程序或内核需要分配一小块内存时,通常会以order 0的方式进行分配。

开启KFENCE

在以下业务场景中经常会用到开启KFENCE功能,具体说明如下:

线上探测场景

场景1:使用KFENCE探测内存是否存在污染

说明

该场景占用2 MiB内存,基本不会影响性能。

  • 添加kfence.sample_interval参数开启KFENCE。

    <kfence.sample_interval>需替换为要配置的值,例如100,表示系统下次启动时自动启用kfence调试工具,并设置其采样间隔为100次事件。

    sudo grubby --update-kernel=/boot/vmlinuz-$(uname -r) --args="kfence.sample_interval=<kfence.sample_interval>"
  • 添加kfence.booting_max根据内存规格控制KFENCE最大的内存消耗。

    说明
    • 内核5.10.134-17及以上版本,新增boot commandline默认配置kfence.booting_max=0-2G:0,2G-32G:2M,32G-:32M用于根据内存规格控制KFENCE最大的内存消耗,配合默认的参数num_objects=255(通常情况下占用2 MiB内存,64 K大页内核占用32 MiB内存),可以控制在任何规格下KFENCE的内存开销均不超过千分之一。

    • 此参数仅控制内存开销的上限,是对num_objects参数的制约,不代表实际内存开销(实际内存开销会小于等于此值)。

    <kfence.booting_max>需替换为要配置的值,例如0-128M:0,128M-256M:1M,256M-:2M,参数说明如下。

    • 机器内存<128 MiB,不开启KFENCE。

    • 128 MiB≤机器内存≤256 MiB,控制KFENCE使用的内存开销上限为1 MiB(对应num_objects上限被制约为127)。

    • 机器内存>256 MiB,控制KFENCE使用的内存开销上限为2 MiB(对应num_objects上限被制约为255)。

    sudo grubby --update-kernel=/boot/vmlinuz-$(uname -r) --args="kfence.booting_max=<kfence.booting_max>"

    此配置仅针对使用boot commandline方式开机自启KFENCE的场景,对于系统已经启动后再配置KFENCE(即场景2)无效。

系统在下次重启时,配置会自动生效。

场景2:使用KFENCE捕获内存污染问题

重要

该场景将消耗以GiB为单位的大量内存,小规格机器慎用。

  1. 新建一个内存分配脚本,并添加如下内容(以脚本名称kfence.sh,监控的目标slab类型是kmalloc-64为例)。

    #!/bin/bash
    # usage: ./kfence.sh kmalloc-64
    
    SLAB_PREFIX=/sys/kernel/slab
    MODULE_PREFIX=/sys/module/kfence/parameters
    
    if [ $# -eq 0 ]; then
    	echo "err: please input slabs"
    	exit 1
    fi
    
    #check whether slab exists
    for i in $@; do
    	slab_path=$SLAB_PREFIX/$i
    	if [ ! -d $slab_path ]; then
    		echo "err: slab $i not exist!"
    		exit 1
    	fi
    done
    
    #calculate num_objects
    sumobj=0
    for i in $@; do
    	objects=($(cat $SLAB_PREFIX/$i/objects))
    	maxobj=1
    	for ((j=1; j<${#objects[@]}; j++)); do
    		nodeobj=$(echo ${objects[$j]} | awk -F= '{print $2}')
    		[ $maxobj -lt $nodeobj ] && maxobj=$nodeobj
    	done
    	((sumobj += maxobj))
    done
    echo "recommend num_objects per node: $sumobj"
    
    #check kfence stats
    if [ $(cat $MODULE_PREFIX/sample_interval) -ne 0 ]; then
    	echo "kfence is running, disable it and wait..."
    	echo 0 > $MODULE_PREFIX/sample_interval
    	sleep 1
    fi
    
    #disable all slabs catching
    for file in $SLAB_PREFIX/*
    do
    	(echo 0 > $file/kfence_enable) 2>/dev/null || echo 1 > $file/skip_kfence
    done
    
    #disable order0 page catching
    echo 0 > $MODULE_PREFIX/order0_page
    
    #enable setting slabs catching
    for i in $@; do
    	(echo 1 > $SLAB_PREFIX/$i/kfence_enable) 2>/dev/null || echo 0 > $SLAB_PREFIX/$i/skip_kfence
    done
    
    #setting num_objects and node mode
    echo $sumobj > $MODULE_PREFIX/num_objects
    echo node > $MODULE_PREFIX/pool_mode
    
    #start kfence
    echo -1 > $MODULE_PREFIX/sample_interval
    if [ $? -ne 0 ]; then
    	echo "err: kfence enable fail!"
    	exit 1
    fi
    echo "kfence enabled!"

    该脚本将探测目标slab的活跃对象数量,并根据该数量估算出合适的KFENCE池子大小,然后启用KFENCE以捕获所有目标slab的分配。

    说明

    slab是内存管理中常用的概念和技术,用于优化内存的分配和释放操作,提高系统的性能和效率。KFENCE支持监控slab以及order 0单页。

  2. 运行以下命令,执行脚本,开始探测。

    sudo bash ./kfence.sh kmalloc-64

线下调试场景

通过添加参数开启KFENCE(x86架构

  1. 运行以下命令,开启KFENCE。

    sudo grubby --update-kernel=/boot/vmlinuz-$(uname -r) --args="kfence.num_objects=1000000"
    sudo grubby --update-kernel=/boot/vmlinuz-$(uname -r) --args="kfence.sample_interval=-1"
    sudo grubby --update-kernel=/boot/vmlinuz-$(uname -r) --args="kfence.fault=panic"
    • num_objects:决定了KFENCE池子的大小。

      • num_objects≤131071时,其占用内存为(num_objects + 1)* 8 KiB。

      • num_objects>131071时,其占用内存为⌈num_objects / 131071⌉ GiB(⌈⌉表示向上取整)。

        说明

        建议该值配置为最大可用内存的10%。例如设置num_objects为1,000,000时,占用内存为⌈1,000,000 / 131071⌉ GiB = 8 GiB。

    • sample_interval:取值包含以下三种情况。

      • 0:表示关闭KFENCE功能。

      • 正数:采样间隔(ms),例如设置为100时,表示每隔100 ms分配的内存将进入KFENCE的监控范围内。

      • 负数:全量模式,所有符合(slab类型筛选)条件的内存均将进入KFENCE的监控范围内。

    • fault:自内核版本5.10.134-16开始新增的参数,默认是report。当设置为panic时,会在捕获问题的现场宕机,以保留第一现场的内核转储文件。

  2. 重启系统使配置生效。

    具体操作,请参见重启实例

通过配置脚本开启KFENCE(x86/ARM架构

说明
  • 通过该方式开启KFENCE时,无法捕获内核启动过程中可能出现的内存污染问题。

  • 开启KFENCE后,如果需要修改num_objectssample_interval配置,需先关闭KFENCE再进行修改。

运行以下命令,开启KFENCE。

sudo sh -c 'echo 1000000 > /sys/module/kfence/parameters/num_objects'
sudo sh -c 'echo -1 > /sys/module/kfence/parameters/sample_interval'
sudo sh -c 'echo panic > /sys/module/kfence/parameters/fault'
  • num_objects:决定了KFENCE池子的大小。其占用内存为 ⌈num_objects / 131071⌉ GiB(⌈⌉表示向上取整)。

    说明

    建议该值配置为最大可用内存的10%。例如设置num_objects为1,000,000时,占用内存为⌈1,000,000 / 131071⌉ GiB = 8 GiB。

  • sample_interval:取值包含以下三种情况。

    • 0:表示关闭KFENCE功能。

    • 正数:采样间隔(ms),例如设置为100时,表示每隔100 ms分配的内存将进入KFENCE的监控范围内。

    • 负数:全量模式,所有符合(slab类型筛选)条件的内存均将进入KFENCE的监控范围内。

  • fault:自内核版本5.10.134-16开始新增的参数,默认是report。当设置为panic时,会在捕获问题的现场宕机,以保留第一现场的内核转储文件。

    说明

    如果您的内核是5.10.134-16之前的版本,执行该条命令会报错,您直接忽略即可,不影响开启KFENCE。

查看结果

KFENCE捕获到内存污染问题后,您可以查看捕获到的问题个数以及详细的错误信息。

  • 查看捕获问题的个数。

    sudo cat /sys/kernel/debug/kfence/stats

    结果如下图所示,total bugs计数增加。

    image.png

  • 查看详细错误信息。

    dmesg | grep -i kfence

    如下图所示,查询到1条错误信息。

    image.png

关闭KFENCE

  • 运行以下命令,关闭KFENCE。

    sudo bash -c 'echo 0 > /sys/module/kfence/parameters/sample_interval'

    关闭KFENCE功能后,KFENCE不再捕获任何内存分配。当池子内监控的所有内存均释放后,KFENCE将以1 GiB单位为粒度向内核伙伴系统返还内存。

  • 通过添加boot commandline参数开启KFENCE的场景,您可以运行以下命令移除这些参数,下次系统重启将不再自动打开KFENCE。

    sudo grubby --update-kernel=/boot/vmlinuz-$(uname -r) --remove-args="kfence.sample_interval"

常见问题

  • KFENCE功能对内存和性能有什么影响?

    • 对内存的影响

      KFENCE采用以大量内存开销换取较小的性能干扰的思路,占用的内存较高。对于重启开启的采样模式(上游Linux同款)可设定任意较小的num_objects来节约内存。其他情况(全量模式及动态开启)则需消耗GiB级别的内存,所以小规格机器慎用。

    • 对性能的影响

      • 采样模式下,对性能影响较小。

      • 全量模式如果筛选得当(例如只打开某种类型的slab监控),一般来说对性能的影响也可以接受。

      说明
      • 建议您根据实际的业务场景先进行灰度测试,观察开启KFENCE后对实际业务性能的影响,再决定后续部署情况。

      • 在线下调试场景中,如果采用全量模式以及全种类监控,性能和内存影响都很大,但是这种场景一般是为了锁定问题,无需关注性能影响。

  • KFENCE功能与KASAN功能有什么区别?

    KFENCE与KASAN都是Linux内核自带的用于捕获内存污染的工具。阿里云在5.10内核中对KFENCE功能进行了增强,除了开关更加灵活、支持采样从而可以在线上的业务环境运行之外,它们在功能上还有以下区别:

    • KFENCE支持监控大小不超过4 KiB的slab(例如kmalloc-4k)以及order 0单页。而KASAN支持监控的内存种类更多,包括所有的slab、page、堆栈和全局内存等。

    • KFENCE对于监控范围内的内存异常行为捕获成功率高于KASAN。

    • KFENCE在内存开销方面高于KASAN,但通常情况下对业务的性能的影响小于KASAN。

    通常情况下,不建议同时使用KFENCE和KASAN功能,KFENCE会接管KASAN的监控目标。

  • KFENCE功能的稳定性怎么样?

    在内核版本5.10.134-15及其之前的版本存在一个已知问题:order 0单页和slab混合监控时,在特定场景下可能造成宕机。您可以运行以下命令,禁用order 0单页监控来预防此问题。

    sudo grubby --update-kernel=/boot/vmlinuz-$(uname -r) --args="kfence.order0_page=0"