频繁设置CGroup触发linux内核bug导致CGroup running task不调度

2023-05-16


stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/time.h>
#include <string>

using namespace std;
std::string sub_cgroup_dir("/sys/fs/cgroup/cpu/test");

// common lib
bool is_dir(const std::string& path)
{
    struct stat statbuf;
    if (stat(path.c_str(), &statbuf) == 0 )
    {
        if (0 != S_ISDIR(statbuf.st_mode))
        {
            return true;
        }
    }
    return false;
}

bool write_file(const std::string& file_path, int num)
{
    FILE* fp = fopen(file_path.c_str(), "w");
/*    if (fp = NULL) 原文 此写法危险,且写错了了,导致fp被置空 */
    if (NULL == fp)
    {
        return false;
    }

    std::string write_data = to_string(num);
    fputs(write_data.c_str(), fp);
    fclose(fp);
    return true;
}

// ms
long get_ms_timestamp()
{
    timeval tv;
    gettimeofday(&tv, NULL);
    return (tv.tv_sec * 1000 + tv.tv_usec / 1000);
}

// cgroup
bool create_cgroup()
{
    if (is_dir(sub_cgroup_dir) == false)
    {
        if (mkdir(sub_cgroup_dir.c_str(), S_IRWXU | S_IRGRP) != 0)
        {
            cout << "mkdir cgroup dir fail" << endl;
            return false;
        }
    }

    int pid = getpid();
    cout << "pid is " << pid << endl;
    std::string procs_path = sub_cgroup_dir + "/cgroup.procs";
    return write_file(procs_path, pid);
}

bool set_period(int period)
{
    std::string period_path = sub_cgroup_dir + "/cpu.cfs_period_us";
    return write_file(period_path, period);
}

bool set_quota(int quota)
{
    std::string quota_path = sub_cgroup_dir + "/cpu.cfs_quota_us";
    return write_file(quota_path, quota);
}

// thread
// param: ms interval
void* thread_func(void* param)
{
    int i = 0;
    int interval = (long)param;
    long last = get_ms_timestamp();

    while (true)
    {
        i++;
        if (i % 1000 != 0)
        {
            continue;
        }

        long current = get_ms_timestamp();
        if ((current - last) >= interval)
        {
            usleep(1000);
            last = current;
        }
    }

    pthread_exit(NULL);
}

 void test_thread()
 {
    const int k_thread_num = 10;
    pthread_t pthreads[k_thread_num];

    for (int i = 0; i < k_thread_num; i++)
    {
        if (pthread_create(&pthreads[i], NULL, thread_func, (void*)(i + 1)) != 0)
        {
            cout << "create thread fail" << endl;
        }
        else
        {
            cout << "create thread success,tid is " << pthreads[i] << endl;
        }
    }
}

//argv[0] : period
//argv[1] : quota
int main(int argc,char* argv[])
{
    if (argc <3)
    {
        cout << "usage : ./inactive timer $period $quota" << endl;
        return -1;
    }

    int period = stoi(argv[1]);
    int quota = stoi(argv[2]);
    cout << "period is " << period << endl;
    cout << "quota is " << quota << endl;

    test_thread();
    if (create_cgroup() == false)
    {
        cout << "create cgroup fail" << endl;
        return -1;
    }

    int i =0;
    while (true)
    {
        if (i > 20)
        {
            i = 0;
        }

        i++;
        long current = get_ms_timestamp();
        long last = current;
        while ((current - last) < i)
        {
            usleep(1000);
            current = get_ms_timestamp();
        }
        
        set_period(period);
        set_quota(quota);
    }

    return 0;
}  

2.1.2 编译


g++ -std=c++11 -lpthread trigger_cgroup_timer_inactive.cpp -o inactive_timer  

2.1.3 在CentOS7.0~7.5的系统上执行程序


./inactive_timer 100000 10000  

2.1.4 上述代码主要干了2件事

1> 将自己进程设置为CGroup控制cpu

2> 反复设置CGroup的cpu.cfs_period_us和cpu.cfs_quota_us

3> 起10个线程消耗cpu

2.1.5《极简组调度-CGroup如何限制cpu》已经讲过CGroup限制cpu的原理:

CGroup控制cpu是通过cfs_period_us指定的一个时间周期内,CGroup下的进程,能使用cfs_quota_us时间长度的cpu,如果在该周期内使用的cpu超过了cfs_quota_us设定的值,则将其throttled,即将其从公平调度运行队列中移出,然后等待定时器触发下个周期unthrottle后再移入,从而达到控制cpu的效果。

2.2 现象

1> 程序跑几分钟后,所有的线程一直处于running状态,但实际线程都已经不再执行了,cpu使用率也一直是0

2> 查看线程的stack,task都在系统调用返回中

3> 用crash查看进程的主线程32764状态确实为"running",但对应的0号cpu上的rq cfs运行队列中并没有任何运行task

4> 查看task对应的se没有在rq上,cfs_rq显示被throttled

《极简组调度-CGroup如何限制cpu》中说过,throttle后经过一个period(程序设的是100ms),CGroup的定时器会再次分配quota,并unthrottle,将group se重新加入到rq中,这里一直throttle不恢复,只能怀疑是不是定时器出问题了

5> 再查看task group对应的cfs_bandwidth的period timer,发现state为0,即HRTIMER_STATE_INACTIVE,表示未激活,问题就在这里,正常情况下该timer是激活的,该定时器未激活会导致对应cpu上的group cfs_rq分配不到quota,quota用完后就会导致其对应的se被移出rq,此时task虽然处于Ready状态,但由于不在rq上,仍然不会被调度的

3. 原因

3.1 linux的定时器是一次性,到期后需要再次激活才能继续使用,搜索代码可知period_timer是在__start_cfs_bandwidth()中实现调用start_bandwidth_timer()进行激活的

这里有一个关键点,当cfs_b->timer_active不为0时,__start_cfs_bandwidth()就会不激活period_timer,和问题现象相符,那么什么时候cfs_b->timer_active会不为0呢?

3.2 当设置CGroup的quota或者period时,会最终进入到__start_cfs_bandwidth(),这里就会将cfs_b->timer_active设为0,并进入__start_cfs_bandwidth()


tg_set_cfs_quota()
    tg_set_cfs_bandwidth()
            /* restart the period timer (if active) to handle new period expiry */
            if (runtime_enabled && cfs_b->timer_active) {
                /* force a reprogram */
                cfs_b->timer_active = 0;
                __start_cfs_bandwidth(cfs_b);
            }  

仔细观察上述代码,设想如下场景:

1> 在线程A设置CGroup的quota或者period时,将cfs_b->timer_active设为0,调用_start_cfs_bandwidth()后,在未执行到__start_cfs_bandwidth()代码580行hrtimer_cancel()之前,cpu切换到B线程

2> 线程B也调用__start_cfs_bandwidth(),执行完后将cfs_b->timer_active设为1,并调用start_bandwidth_timer()激活timer,此时cpu切换到线程A

3> 线程A恢复并继续执行,调用hrtimer_cancel()让period_timer失效,然后执行到__start_cfs_bandwidth()代码585行后,发现cfs_b->timer_active为1,直接return,而不再将period_timer激活

3.3 搜索__start_cfs_bandwidth()的调用,发现时钟中断中会调用update_curr()函数,其最终会调用assign_cfs_rq_runtime()检查cgroup cpu配额使用情况,决定是否需要throttle,这里在cfs_b->timer_active = 0时,也会调用__start_cfs_bandwidth(),即执行上面B线程的代码,从而和设置CGroup的线程A发生线程竞争,导致timer失效。

1> 完整代码执行流程图

2> 当定时器失效后,由于3.2中线程B将cfs_b->timer_active = 1,所以即使下次时钟中断执行到assign_cfs_rq_runtime()中时,由于误判timer是active的,也不会调用__start_cfs_bandwidth()再次激活timer,这样被throttle的group se永远不会被unthrottle投入rq调度了

3.4 总结

频繁设置CGroup配置,会和时钟中断中检查group quota的线程在__start_cfs_bandwidth()上发生线程竞争,导致period_timer被cancel后不再激活,然后CGroup控制的task不能分配cpu quota,导致不再被调度

3.5 恢复方法

知道了漏洞成因,我们也看到tg_set_cfs_quota()会调用__start_cfs_bandwidth() cancel掉timer,然后重新激活timer,这样就能在timer回调中unthrottle了,所以只要手动设置下这个CGroup的cpu.cfs_period_us或cpu.cfs_quota_us,就能恢复运行。

4. 修复

3.10.0-693以上的版本并不会出现这个问题,通过和2.6.32版本(下图右边)的代码对比,可知3.10.0-693版的代码(下图左边)将hrtimer_cancel()该为hrtimer_try_to_cancel(),并将其和cfs_b->timer_active的判定都放在自旋锁中保护,这样就不会cfs_b->timer_active被置1后,仍然还会去cancel period_timer的问题了,但看这个bug fix的邮件组讨论,是为了修另一个问题顺便把这个问题也修了,痛失给linux提patch的机会- -

ref : https://gfiber.googlesource.com/kernel/bruno/+/09dc4ab03936df5c5aa711d27c81283c6d09f495

5. 漏洞利用

1> 在国内,仍有大量的公司在使用CentOS6和CentOS7.0~7.5,这些系统都存在这个漏洞,使用了CGroup限制cpu就有可能触发这个bug导致业务中断,且还不一定能重启恢复

2> 一旦触发这个bug,由于task本身已经是running状态了,即使去kill,由于task得不到调度,是无法kill掉的,因此可以通过这种方法攻击任意软件程序(如杀毒软件),让其不能执行又不能重启(很多程序为了保证不双开,都会只保证只有一个进程存在),即使他们不用CGroup,也可以给他建一个对其进行攻击

3> 该bug由于是linux内核bug,一旦触发还不易排查和感知,因为看进程状态都是running,直觉上认为进程仍然在正常执行的

本文为博主原创文章,如需转载请说明转至http://www.cnblogs.com/organic/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

频繁设置CGroup触发linux内核bug导致CGroup running task不调度 的相关文章

  • 如何通过代理将套接字连接到http服务器?

    最近 我使用 C 语言编写了一个程序 用于连接到本地运行的 HTTP 服务器 从而向该服务器发出请求 这对我来说效果很好 之后 我尝试使用相同的代码连接到网络上的另一台服务器 例如 www google com 但我无法连接并从网络中的代理
  • 如何确保 numpy BLAS 库可用作动态加载库?

    The theano安装文档 http www deeplearning net software theano install html troubleshooting make sure you have a blas library指
  • 如何从类似于 eclipse 的命令行创建可运行的 jar 文件

    我知道 eclipse 会生成一个可运行的 jar 文件 其中提取并包含在该 jar 文件中的所有库 jar 文件 从命令提示符手动创建 jar 文件时如何执行类似的操作 我需要将所有 lib jar 解压到类文件夹中吗 目前我正在使用 j
  • 确保 config.h 包含一次

    我有一个库项目 正在使用 Linux 中的 autotools 套件移植到该项目 我对自动工具很陌生 本周 我已经了解了其操作的基础知识 我有一个关于如何保留内容的问题config h免遭重新定义 我惊讶地发现生成的config h文件也没
  • 套接字:监听积压并接受

    listen sock backlog 在我看来 参数backlog限制连接数量 这是我的测试代码 server initialize the sockaddr of server server sin family AF INET ser
  • 如何成功使用RDAP协议代替whois

    我对新的 RDAP 协议有点困惑 也不知道何时进一步追求它有意义 在我看来 每个人都同意它是 whois 的继承者 但他们的数据库似乎是空的 在 ubuntu 上我尝试了 rdapper nicinfo 甚至他们的 RESTful API
  • 如何使用AWK脚本检查表的所有列数据类型? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 在这里 我正在检查表中第一列的数据类型 但我想知道AWK中表的所有列数据类型 我尝试过 但只能获得一列数据类型 例如 Column 1
  • 在 LINUX 上使用 Python 连接到 OLAP 多维数据集

    我知道如何在 Windows 上使用 Python 连接到 MS OLAP 多维数据集 嗯 至少有一种方法 通常我使用 win32py 包并调用 COM 对象进行连接 import win32com client connection wi
  • Linux 中热插拔设备时检测设备是否存在

    我正在运行 SPIcode http lxr free electrons com source drivers spi spi omap2 mcspi c在熊猫板上 我想知道其中的哪个功能code http lxr free electr
  • 每个进程每个线程的时间量

    我有一个关于 Windows 和 Linux 中进程和线程的时间量子的问题 我知道操作系统通常为每个线程提供固定的时间量 我知道时间量根据前台或后台线程而变化 也可能根据进程的优先级而变化 每个进程有固定的时间量吗 例如 如果操作系统为每个
  • 如何让“grep”从文件中读取模式?

    假设有一个很大的文本文件 我只想打印与某些模式不匹配的行 显然 我可以使用egrep v patter1 pattern2 pattern3 现在 如果所有这些模式都在一个文本文件中怎么办 最好的制作方法是什么egrep从文件中读取模式 g
  • 从 TypeScript 运行任何 Linux 终端命令?

    有没有办法直接从 TypeScript 类中执行 Linux 终端命令 这个想法是做类似的事情 let myTerminal new LinuxTerminal let terminalResult myTerminal run sudo
  • sleep 0 有特殊含义吗?

    我看到很多用法sleep 0在我的一个客户项目中 代码看起来像这样 while true sleep 0 end 阅读一些像这样的答案this https stackoverflow com questions 3727420 signif
  • C 程序从连接到系统的 USB 设备读取数据

    我正在尝试从连接到系统 USB 端口的 USB 设备 例如随身碟 获取数据 在这里 我可以打开设备文件并读取一些随机原始数据 但我想获取像 minicom teraterm 这样的数据 请让我知道我可以使用哪些方法和库来成功完成此操作以及如
  • 适用于多应用项目的 Grunt 和 requirejs 优化器

    我在让 Grunt 对具有以下结构的项目执行 requirejs 优化时遇到问题 static js apps app js dash js news js many more app files build collections lib
  • 为什么 Linux 对目录使用 getdents() 而不是 read()?

    我浏览 K R C 时注意到 为了读取目录中的条目 他们使用了 while read dp gt fd char dirbuf sizeof dirbuf sizeof dirbuf code Where dirbuf是系统特定的目录结构
  • 归档文件系统或格式

    我正在寻找一种文件类型来存储已退役系统的档案 目前 我们主要使用 tar gz 但从 200GB tar gz 存档中查找并提取几个文件是很麻烦的 因为 tar gz 不支持任何类型的随机访问读取规定 在你明白之前 使用 FUSE 安装 t
  • 如何制作和应用SVN补丁?

    我想制作一个SVN类型的补丁文件httpd conf这样我就可以轻松地将其应用到其他主机上 If I do cd root diff Naur etc httpd conf httpd conf original etc httpd con
  • 并行运行 make 时出错

    考虑以下制作 all a b a echo a exit 1 b echo b start sleep 1 echo b end 当运行它时make j2我收到以下输出 echo a echo b start a exit 1 b star
  • 在 Linux 中禁用历史记录 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 要在 Linux 环境中禁用历史记录 我执行了以下命令 export HISTFILESIZE 0 export HISTSIZE 0 u

随机推荐