頻繁設置CGroup觸發linux內核bug導致CGroup running task不調度

来源:https://www.cnblogs.com/organic/archive/2023/04/15/17321523.html
-Advertisement-
Play Games

title: msp430點燈實驗 date: 2023-04-15 15:31:25 description: 基於msp430f5529點燈實驗 一、實驗內容 使用開發板:msp430f5529 使用的LED燈:為開發板上自帶的User LEDs(LED1、LED2) 環境:CCS (Versi ...


1. 說明 1> 本篇是實際工作中linux上碰到的一個問題,一個使用了CGroup的進程處於R狀態但不執行,也不退出,還不能kill,經過深入挖掘才發現是Cgroup的內核bug 2>發現該bug後,去年給RedHat提交過漏洞,但可惜並未通過,不知道為什麼,這裡就發我博客公開了 3> 前面的2個帖子《極簡cfs公平調度演算法》《極簡組調度-CGroup如何限制cpu》是為了瞭解本篇這個內核bug而寫的,需要linux內核進程調度和CGroup控制的基本原理才能夠比較清晰的瞭解這個內核bug的來龍去脈 4> 本文所用的內核調試工具是crash,大家可以到官網上去查看crash命令的使用,這裡就不多介紹了 https://crash-utility.github.io/help.html   2. 問題 2.1 觸發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

 

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/
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如,序列 {1,2,3,4,5} 是某棧的壓棧序列,序列 {4,5,3,2,1} 是該壓棧序列對應的一個彈出序列,但 {4,3,5,1,2} 就不可能是該壓棧序列的彈出序列。 示例 1 ...
  • JVM記憶體分配 先瞭解下JVM中的記憶體分配,此處以hotspot vm為例(官方jdk採用的vm) 程式計數器 棧 1. 虛擬機棧 2. 本地方法棧 Java堆 堆記憶體是各個線程共用的區域 方法區 它用於存儲已經被虛擬機載入的類信息、常量、靜態變數、即編譯器編譯後的代碼等數據。靜態變數、常量在方法區 ...
  • 根據不同的條件,調用不同的 bean 對象,執行對象中的方法 SpringUtils 工具類 package com.vipsoft.web.utils; import cn.hutool.core.util.ArrayUtil; import org.springframework.aop.fra ...
  • 本系列文章導航 https://www.cnblogs.com/aierong/p/17300066.html https://github.com/aierong/WpfDemo (自我Demo地址) 希望提到的知識對您有所提示,同時歡迎交流和指正 作者:aierong出處:https://www ...
  • 本系列文章導航 https://www.cnblogs.com/aierong/p/17300066.html https://github.com/aierong/WpfDemo (自我Demo地址) 希望提到的知識對您有所提示,同時歡迎交流和指正 作者:aierong出處:https://www ...
  • 安裝 NuGet 包:在 Visual Studio 中打開項目,右鍵單擊項目名稱,選擇“管理 NuGet 包”,搜索“Quartz”並安裝。 創建作業:創建一個實現了 IJob 介面的類,該介面包含一個 Execute 方法,該方法將在作業運行時調用。例如: public class MyJob ...
  • 朋友做網站需要根據城市展示天氣預報,找了一圈沒有找到靠譜的介面,今天在中央氣象臺的官網查詢某個城市找到了介面,先用postman試了一下居然可以使用,可以查詢某個城市7天的天氣預報等信息。但是查詢編碼是氣象臺自己的編碼,在網上搜索了一下居然有這個編碼。本文使用HttpClient方法查詢這個介面。 ...
  • #總覽需求 1. 簡述靜態網頁和動態網頁的區別。 2. 簡述 Webl.0 和 Web2.0 的區別。 3. 安裝tomcat8,配置服務啟動腳本,部署jpress應用。 1、簡述靜態網頁和動態網頁的區別 靜態網頁: 請求響應信息,發給客戶端進行處理,由瀏覽器進行解析,顯示的頁面,靜態網頁包含文本、 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...