4

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

 1 year ago
source link: https://www.cnblogs.com/organic/p/17321523.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

  博客园 ::

首页 :: 博问 :: 闪存 ::

新随笔 ::

联系 ::

订阅

::

管理 ::

  65 随笔 :: 0 文章 :: 12 评论 ::

13万 阅读

1. 说明
1> 本篇是实际工作中linux上碰到的一个问题,一个使用了CGroup的进程处于R状态但不执行,也不退出,还不能kill,经过深入挖掘才发现是Cgroup的内核bug
2>发现该bug后,去年给RedHat提交过漏洞,但可惜并未通过,不知道为什么,这里就发我博客公开了
3> 前面的2个帖子《极简cfs公平调度算法》《极简组调度-CGroup如何限制cpu》是为了了解本篇这个内核bug而写的,需要linux内核进程调度和CGroup控制的基本原理才能够比较清晰的了解这个内核bug的来龙去脉
4> 本文所用的内核调试工具是crash,大家可以到官网上去查看crash命令的使用,这里就不多介绍了
2. 问题
2.1 触发bug code(code较长,请展开代码)
2.1.1 code

ContractedBlock.gifExpandedBlockStart.gif

View Code

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 现象
818872-20230415173929251-1217747819.png
1> 程序跑几分钟后,所有的线程一直处于running状态,但实际线程都已经不再执行了,cpu使用率也一直是0
2> 查看线程的stack,task都在系统调用返回中
818872-20230415173929248-1384460606.png
3> 用crash查看进程的主线程32764状态确实为"running",但对应的0号cpu上的rq cfs运行队列中并没有任何运行task
818872-20230415173929346-389924953.png
4> 查看task对应的se没有在rq上,cfs_rq显示被throttled
818872-20230415173929347-1958393815.png
818872-20230415173929273-1926683778.png
《极简组调度-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上,仍然不会被调度的
818872-20230415173929311-288335838.png
3. 原因
3.1 linux的定时器是一次性,到期后需要再次激活才能继续使用,搜索代码可知period_timer是在__start_cfs_bandwidth()中实现调用start_bandwidth_timer()进行激活的
818872-20230415173929373-1384389752.png
这里有一个关键点,当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激活
818872-20230415173929312-596333791.png
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> 完整代码执行流程图
818872-20230415190752064-12523573.png
2> 当定时器失效后,由于3.2中线程B将cfs_b->timer_active = 1,所以即使下次时钟中断执行到assign_cfs_rq_runtime()中时,由于误判timer是active的,也不会调用__start_cfs_bandwidth()再次激活timer,这样被throttle的group se永远不会被unthrottle投入rq调度了
818872-20230415173929275-317826516.png
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的机会- -
17321523.html
818872-20230415173929292-1442914109.png
5. 漏洞利用
1> 在国内,仍有大量的公司在使用CentOS6和CentOS7.0~7.5,这些系统都存在这个漏洞,使用了CGroup限制cpu就有可能触发这个bug导致业务中断,且还不一定能重启恢复
2> 一旦触发这个bug,由于task本身已经是running状态了,即使去kill,由于task得不到调度,是无法kill掉的,因此可以通过这种方法攻击任意软件程序(如杀毒软件),让其不能执行又不能重启(很多程序为了保证不双开,都会只保证只有一个进程存在),即使他们不用CGroup,也可以给他建一个对其进行攻击
3> 该bug由于是linux内核bug,一旦触发还不易排查和感知,因为看进程状态都是running,直觉上认为进程仍然在正常执行的

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK