這篇具有很好參考價(jià)值的文章主要介紹了頻繁設(shè)置CGroup觸發(fā)linux內(nèi)核bug導(dǎo)致CGroup running task不調(diào)度。希望對大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。
1. 說明
1> 本篇是實(shí)際工作中l(wèi)inux上碰到的一個(gè)問題,一個(gè)使用了CGroup的進(jìn)程處于R狀態(tài)但不執(zhí)行,也不退出,還不能kill,經(jīng)過深入挖掘才發(fā)現(xiàn)是Cgroup的內(nèi)核bug
2>發(fā)現(xiàn)該bug后,去年給RedHat提交過漏洞,但可惜并未通過,不知道為什么,這里就發(fā)我博客公開了
3> 前面的2個(gè)帖子《極簡cfs公平調(diào)度算法》《極簡組調(diào)度-CGroup如何限制cpu》是為了了解本篇這個(gè)內(nèi)核bug而寫的,需要linux內(nèi)核進(jìn)程調(diào)度和CGroup控制的基本原理才能夠比較清晰的了解這個(gè)內(nèi)核bug的來龍去脈
4> 本文所用的內(nèi)核調(diào)試工具是crash,大家可以到官網(wǎng)上去查看crash命令的使用,這里就不多介紹了
https://crash-utility.github.io/help.html
?
2. 問題
2.1 觸發(fā)bug code(code較長,請展開代碼)
2.1.1 code


#include <iostream>
#include <sys/types.h>
#include <signal.h>
#include <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)
{
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;
}
View Code
?文章來源地址http://www.zghlxwxcb.cn/news/detail-414430.html
2.1.2 編譯
g++ -std=c++11 -lpthread trigger_cgroup_timer_inactive.cpp -o inactive_timer
?
2.1.3 在CentOS7.0~7.5的系統(tǒng)上執(zhí)行程序
./inactive_timer 100000 10000
?
2.1.4 上述代碼主要干了2件事
1> 將自己進(jìn)程設(shè)置為CGroup控制cpu
2> 反復(fù)設(shè)置CGroup的cpu.cfs_period_us和cpu.cfs_quota_us
3> 起10個(gè)線程消耗cpu
?
2.1.5《極簡組調(diào)度-CGroup如何限制cpu》已經(jīng)講過CGroup限制cpu的原理:
CGroup控制cpu是通過cfs_period_us指定的一個(gè)時(shí)間周期內(nèi),CGroup下的進(jìn)程,能使用cfs_quota_us時(shí)間長度的cpu,如果在該周期內(nèi)使用的cpu超過了cfs_quota_us設(shè)定的值,則將其throttled,即將其從公平調(diào)度運(yùn)行隊(duì)列中移出,然后等待定時(shí)器觸發(fā)下個(gè)周期unthrottle后再移入,從而達(dá)到控制cpu的效果。
?
2.2 現(xiàn)象
1> 程序跑幾分鐘后,所有的線程一直處于running狀態(tài),但實(shí)際線程都已經(jīng)不再執(zhí)行了,cpu使用率也一直是0
?
2> 查看線程的stack,task都在系統(tǒng)調(diào)用返回中
?
3> 用crash查看進(jìn)程的主線程32764狀態(tài)確實(shí)為"running",但對應(yīng)的0號cpu上的rq cfs運(yùn)行隊(duì)列中并沒有任何運(yùn)行task
?
4> 查看task對應(yīng)的se沒有在rq上,cfs_rq顯示被throttled
《極簡組調(diào)度-CGroup如何限制cpu》中說過,throttle后經(jīng)過一個(gè)period(程序設(shè)的是100ms),CGroup的定時(shí)器會(huì)再次分配quota,并unthrottle,將group se重新加入到rq中,這里一直throttle不恢復(fù),只能懷疑是不是定時(shí)器出問題了
?
5> 再查看task group對應(yīng)的cfs_bandwidth的period timer,發(fā)現(xiàn)state為0,即HRTIMER_STATE_INACTIVE,表示未激活,問題就在這里,正常情況下該timer是激活的,該定時(shí)器未激活會(huì)導(dǎo)致對應(yīng)cpu上的group cfs_rq分配不到quota,quota用完后就會(huì)導(dǎo)致其對應(yīng)的se被移出rq,此時(shí)task雖然處于Ready狀態(tài),但由于不在rq上,仍然不會(huì)被調(diào)度的
?
3. 原因
3.1 linux的定時(shí)器是一次性,到期后需要再次激活才能繼續(xù)使用,搜索代碼可知period_timer是在__start_cfs_bandwidth()中實(shí)現(xiàn)調(diào)用start_bandwidth_timer()進(jìn)行激活的
這里有一個(gè)關(guān)鍵點(diǎn),當(dāng)cfs_b->timer_active不為0時(shí),__start_cfs_bandwidth()就會(huì)不激活period_timer,和問題現(xiàn)象相符,那么什么時(shí)候cfs_b->timer_active會(huì)不為0呢?
?
3.2 當(dāng)設(shè)置CGroup的quota或者period時(shí),會(huì)最終進(jìn)入到__start_cfs_bandwidth(),這里就會(huì)將cfs_b->timer_active設(shè)為0,并進(jìn)入__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);
}
仔細(xì)觀察上述代碼,設(shè)想如下場景:
1> 在線程A設(shè)置CGroup的quota或者period時(shí),將cfs_b->timer_active設(shè)為0,調(diào)用_start_cfs_bandwidth()后,在未執(zhí)行到__start_cfs_bandwidth()代碼580行hrtimer_cancel()之前,cpu切換到B線程
2> 線程B也調(diào)用__start_cfs_bandwidth(),執(zhí)行完后將cfs_b->timer_active設(shè)為1,并調(diào)用start_bandwidth_timer()激活timer,此時(shí)cpu切換到線程A
3> 線程A恢復(fù)并繼續(xù)執(zhí)行,調(diào)用hrtimer_cancel()讓period_timer失效,然后執(zhí)行到__start_cfs_bandwidth()代碼585行后,發(fā)現(xiàn)cfs_b->timer_active為1,直接return,而不再將period_timer激活
?
3.3 搜索__start_cfs_bandwidth()的調(diào)用,發(fā)現(xiàn)時(shí)鐘中斷中會(huì)調(diào)用update_curr()函數(shù),其最終會(huì)調(diào)用assign_cfs_rq_runtime()檢查cgroup cpu配額使用情況,決定是否需要throttle,這里在cfs_b->timer_active = 0時(shí),也會(huì)調(diào)用__start_cfs_bandwidth(),即執(zhí)行上面B線程的代碼,從而和設(shè)置CGroup的線程A發(fā)生線程競爭,導(dǎo)致timer失效。
1>?完整代碼執(zhí)行流程圖

?文章來源:http://www.zghlxwxcb.cn/news/detail-414430.html
?
2> 當(dāng)定時(shí)器失效后,由于3.2中線程B將cfs_b->timer_active = 1,所以即使下次時(shí)鐘中斷執(zhí)行到assign_cfs_rq_runtime()中時(shí),由于誤判timer是active的,也不會(huì)調(diào)用__start_cfs_bandwidth()再次激活timer,這樣被throttle的group se永遠(yuǎn)不會(huì)被unthrottle投入rq調(diào)度了
?
3.4 總結(jié)
頻繁設(shè)置CGroup配置,會(huì)和時(shí)鐘中斷中檢查group quota的線程在__start_cfs_bandwidth()上發(fā)生線程競爭,導(dǎo)致period_timer被cancel后不再激活,然后CGroup控制的task不能分配cpu quota,導(dǎo)致不再被調(diào)度
?
3.5 恢復(fù)方法
知道了漏洞成因,我們也看到tg_set_cfs_quota()會(huì)調(diào)用__start_cfs_bandwidth() cancel掉timer,然后重新激活timer,這樣就能在timer回調(diào)中unthrottle了,所以只要手動(dòng)設(shè)置下這個(gè)CGroup的cpu.cfs_period_us或cpu.cfs_quota_us,就能恢復(fù)運(yùn)行。
?
4. 修復(fù)
3.10.0-693以上的版本并不會(huì)出現(xiàn)這個(gè)問題,通過和2.6.32版本(下圖右邊)的代碼對比,可知3.10.0-693版的代碼(下圖左邊)將hrtimer_cancel()該為hrtimer_try_to_cancel(),并將其和cfs_b->timer_active的判定都放在自旋鎖中保護(hù),這樣就不會(huì)cfs_b->timer_active被置1后,仍然還會(huì)去cancel period_timer的問題了,但看這個(gè)bug fix的郵件組討論,是為了修另一個(gè)問題順便把這個(gè)問題也修了,痛失給linux提patch的機(jī)會(huì)- -
ref : https://gfiber.googlesource.com/kernel/bruno/+/09dc4ab03936df5c5aa711d27c81283c6d09f495
?
5. 漏洞利用
1> 在國內(nèi),仍有大量的公司在使用CentOS6和CentOS7.0~7.5,這些系統(tǒng)都存在這個(gè)漏洞,使用了CGroup限制cpu就有可能觸發(fā)這個(gè)bug導(dǎo)致業(yè)務(wù)中斷,且還不一定能重啟恢復(fù)
2> 一旦觸發(fā)這個(gè)bug,由于task本身已經(jīng)是running狀態(tài)了,即使去kill,由于task得不到調(diào)度,是無法kill掉的,因此可以通過這種方法攻擊任意軟件程序(如殺毒軟件),讓其不能執(zhí)行又不能重啟(很多程序?yàn)榱吮WC不雙開,都會(huì)只保證只有一個(gè)進(jìn)程存在),即使他們不用CGroup,也可以給他建一個(gè)對其進(jìn)行攻擊
3> 該bug由于是linux內(nèi)核bug,一旦觸發(fā)還不易排查和感知,因?yàn)榭催M(jìn)程狀態(tài)都是running,直覺上認(rèn)為進(jìn)程仍然在正常執(zhí)行的
到了這里,關(guān)于頻繁設(shè)置CGroup觸發(fā)linux內(nèi)核bug導(dǎo)致CGroup running task不調(diào)度的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!
本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!