【原創】訪問Linux進程文件表導致系統異常複位的排查記錄

来源:https://www.cnblogs.com/smith9527/archive/2019/01/16/10246847.html
-Advertisement-
Play Games

前提知識: Linux內核、Linux 進程和文件數據結構、vmcore解析、彙編語言 問題背景: 這個問題出自項目的一個安全模塊,主要功能是確定某進程是否有許可權訪問其正在訪問的文件。 實現功能時,需要在內核里通過掃描該進程打開的文件表,獲取文件的路徑,和安全模塊里配置的可訪問文件的進程白名單進行匹 ...


前提知識:

   Linux內核、Linux 進程和文件數據結構、vmcore解析、彙編語言

問題背景:

   這個問題出自項目的一個安全模塊,主要功能是確定某進程是否有許可權訪問其正在訪問的文件。

        實現功能時,需要在內核里通過掃描該進程打開的文件表,獲取文件的路徑,和安全模塊里配置的可訪問文件的進程白名單進行匹配;

模塊會一直到搜索到進程pid為1的進程,也就是init進程。在訪問中間某個父進程的文件表時,出現struct task_struct的files指針為空的情況,

導致系統異常複位。

  下麵就是這次異常的分析和定位過程,希望對大家有所幫助,有什麼想法我們可以交流討論。

 

       接到現場保障時,沒想到又是這個模塊導致的,因為這個模塊剛升級版本,首先是確認系統有無crash vmcore文件生成,還好這次有

vmcore文件,先登錄上去看異常堆棧吧。

     

 

  確實是安全內核模塊出了異常,為了保密,我省去了很多信息。

      在dmesg.txt文件里,還有一句話:

   <1>[259267.001561] BUG: unable to handle kernel NULL pointer dereference at 0000000000000008

  從堆棧信息可以產出,異常進程comm名稱是gzip,pid為10877,task指針為ffff88207f7f2380,異常原因是訪問了NULL指針。

 

  通過nm和addr2line命令,我們定位具體出異常的代碼行:

  include/linux/fdtable.h: 87

  

 

   緊接著查看files_fdtable代碼實現,是對files->fdt的訪問:

  #define files_fdtable(files) \

        (rcu_dereference_check_fdtable((files), (files)->fdt))  

  分析到這裡,初步判斷是訪問files執行了異常。我們結合vmcore信息進一步確認。

 

 vmcore文件分析

  查找異常進程的files成員變數值是正常的,如下所示:

  ## crash> struct task_struct.files ffff88207f7f2380
  ##   files = 0xffff881f97cff380

      異常進程的進程名稱:
  ## crash> struct task_struct.comm ffff88207f7f2380
  ##  comm = "gzip\000\000\000\000\000\000\000\000\000\000\000"

     

      問題不是訪問當前進程files導致的,聯想到此模塊會向上遍歷parent進程,並獲取相關files中打開的文件信息,問題可能出自中間過程。

      但是究竟是訪問哪個進程出的問題呢?這就需要查看調用函數的堆棧信息和寄存器信息。

 

查找異常進程的父子進程關係

  通過crash的ps命令,我們可以得到異常時所有的進程信息,我們摘出與gzip相關的進行信息:

     

  從上圖我們可以看到與gzip進程(pid=10877)相關的父子進程關係,我們上溯到pid=10875的進程是,發現其VSZ和RSS都是0,比較可疑。

通過crash該進程信息,可以看到其files,mm變數都為NULL。

  

  從這裡可以推斷可能是訪問該進程的異常files成員變數,導致了系統異常。

 

  到底是不是這個訪問引起的,我們還要從當時的堆棧信息做最終的確認。

       通過crash dis 命令,可以得到 堆棧中顯示的異常函數的彙編代碼,截取代碼片段如下:

 

  異常堆棧顯示異常代碼是fdtable.h line 87,其上面一段代碼: mov 0x730(%rdi),%rax就是裝載task->files變數到rax寄存器。

結合堆棧信息,寄存器rdi值正是訪問的task結構體指針ffff881f1d3ce280,而當前rax寄存器值為0。所以,會引起訪問NULL指針

的異常。

  另外,struct task_struct結構體中,files成員的偏移是1840,也就是0x730。

  crash>  struct task_struct.files
  struct task_struct {
   [1840] struct files_struct *files;
  }

  現在我們完全可以確定,安全模塊函數訪問了進程的files空指針,引起了系統異常。

 

       但是,為什麼父進程的files成員變數會為NULL呢?一般fork出來的子進程都會copy父進程的files等變數的呀。

 關於這個問題,還是要從業務的源代碼分析。業務中做文件壓縮的模擬代碼如下:

 1         pid_t pid;
 2         if ( (pid = vfork())<0 )
 3         {
 4            debug(("fork first process  error.") );
 5         }
 6 
 7         if (pid == 0)
 8         {
 9             if ( (pid = vfork())<0 )
10             {
11                 debug(("fork second process error.") );
12             }
13 
14             if (pid == 0)
15             {
16                 if(execlp("/XXX/mygzip.sh", "-f", ttemp.c_str(), t.c_str(), (char *) 0) <0 )
17                 {
18                     debug(("execlp gzip error.") );
19                 }
20             }
21             _exit(0);
22         }
23         else
24         {
25             if ( waitpid(pid, NULL, 0) <0 )
26             {
27                 debug( ("wait error.") );
28             }
29         }

   業務代碼里通過vfork出來子進程調用execlp執行mygzip.sh腳本來做文件壓縮。

  

       查找vfork函數說明,有如下描述:

       vfork() differs from fork(2) in that the parent is suspended until the child terminates (either normally,  by  calling  exit(2),  or  abnormally,  after
delivery  of a fatal signal), or it makes a call to execve(2).  Until that point, the child shares all memory with its parent, including the stack.  The
child must not return from the current function or call exit(3), but may call _exit(2).

  翻譯一下,就是: 調用vfork的父進程會一直阻塞到子進程終結。  

        分析vfork的內核源碼,也可以得到相應的印證:do_fork會調用copy_process函數,拷貝files,mm,fs等信息;由於vfork調用do_fork是帶有

CLONE_VFORK標記,會等待子進程返回。

1 int sys_vfork(struct pt_regs *regs)
2 {
3     return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->sp, regs, 0,  NULL, NULL);
4 }

 

 1 long do_fork(unsigned long clone_flags,
 2           unsigned long stack_start,
 3           struct pt_regs *regs,
 4           unsigned long stack_size,
 5           int __user *parent_tidptr,
 6           int __user *child_tidptr)
 7 {
 8     ......
 9     /* copy files,mm,fs,namespace等信息 */
10     p = copy_process(clone_flags, stack_start, regs, stack_size,
11              child_tidptr, NULL, trace);
12     /*
13      * Do this prior waking up the new thread - the thread pointer
14      * might get invalid after that point, if the thread exits quickly.
15      */
16     if (!IS_ERR(p)) {
17         struct completion vfork;
18 
19         ......
20 
21         wake_up_new_task(p);
22 
23         tracehook_report_clone_complete(trace, regs,
24                         clone_flags, nr, p);
25 
26         if (clone_flags & CLONE_VFORK) {
27             freezer_do_not_count();
28             wait_for_completion(&vfork);  /* 等待子進程返回 */
29             freezer_count();
30             tracehook_report_vfork_done(p, nr);
31         }
32     } else {
33         nr = PTR_ERR(p);
34     }
35     return nr;
36 }

   

     以上說明,正常情況,子進程執行完成後,父進程才繼續執行,其files,mm等成員不應該為空才對。

          關鍵是,vfork說明裡還有關鍵的一句:

   (either normally,  by  calling  exit(2),  or  abnormally,  after delivery  of a fatal signal), or it makes a call to execve(2). 

   說明子進程返回有三種情況:調用exit返回,或發送致命信號異常返回,或調用execve函數族返回。

   業務代碼調用了execlp函數,子進程啟動mygzip.sh腳本後,立即返回了,父進程等到了子進程退出,也調用了_exit(0)函數。

   接著分析exit函數實現,會發現do_exit函數會釋放父進程的mm,files等數據:        

 1 NORET_TYPE void do_exit(long code)
 2 {
 3     struct task_struct *tsk = current;
 4         ......
 5     exit_signals(tsk);  /* sets PF_EXITING */
 6     ......
 7     exit_mm(tsk);   /* 釋放mm數據 */
 8         ......
 9     exit_sem(tsk);
10     exit_files(tsk);  /* 釋放打開的文件表 */
11     exit_fs(tsk);
12     check_stack_usage();
13     exit_thread();
14 
15     ......
16 }

  

  exit_files函數實現:

 1 void exit_files(struct task_struct *tsk)
 2 {
 3     struct files_struct * files = tsk->files;
 4 
 5     if (files) {
 6         task_lock(tsk);
 7         tsk->files = NULL;  /* 進程files賦值為NULL */
 8         task_unlock(tsk);
 9         put_files_struct(files); /* 會調用close_files函數,接著看下麵的代碼 */
10     }
11 }

 

  put_files_struct函數:

 1 void put_files_struct(struct files_struct *files)
 2 {
 3     struct fdtable *fdt;
 4 
 5     if (atomic_dec_and_test(&files->count)) {
 6         close_files(files);  /* 會調用 cond_resched(); */
 7         ......19     }
20 }

  

closes_files函數:

 1 static void close_files(struct files_struct * files)
 2 {
 3 ......
 4     rcu_read_lock();
 5     fdt = files_fdtable(files);
 6     rcu_read_unlock();
 7     for (;;) {
 8         unsigned long set;
 9         i = j * __NFDBITS;
10         if (i >= fdt->max_fds)
11             break;
12         set = fdt->open_fds->fds_bits[j++];
13         while (set) {
14             if (set & 1) {
15                 struct file * file = xchg(&fdt->fd[i], NULL);
16                 if (file) {
17                     filp_close(file, files);
18                     cond_resched();  /* 正式這一句代碼,讓gzip進程有了執行的機會,父進程此時還未完全退出,但是其files已經是NULL */
19                 }
20             }
21             i++;
22             set >>= 1;
23         }
24     }
25 }

  cond_resched();

       正式這一句代碼,讓gzip進程有了執行的機會,父進程此時還未完全退出,但是其files已經是NULL。當gzip訪問父進程的files變數時,

就會出現NULL訪問異常,系統異常複位。

 

  經過以上的分析,可以得出如下結論:

      1.由於子進程訪問了父進程的空files,導致了系統異常;

      2.由於vfork和execlp函數的特性,共同決定了父進程files值為NULL的可能;

   3.子進程通過parent訪問父進程的成員變數是不安全的。

  

      最後一個問題:如果才能安全訪問進程的parent及其成員變數呢?這又是一個課題了,有待後續分析。

     

 PS:您的支持是對博主最大的鼓勵

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 包括nginx的入門和進階學習。 目錄 nginx系列1:認識nginx nginx系列2:搭建nginx環境 nginx系列3:搭建一個靜態資源web伺服器 nginx系列4:日誌管理 nginx系列5:nginx的請求處理流程 nginx系列6:nginx的進程結構 nginx系列7:處理HTT ...
  • 之前項目當中的接入的高德逆地理編碼功能偶爾會出現參數錯誤的bug,經過排查服務端異常log,發現請求的url中的location參數中的小數點變成了逗號。 代碼如下 其中 lng.ToString(), lat.ToString() 轉換string的時候,偶爾會把中間的點號轉成逗號,於是造成高德a ...
  • 最少連接演算法使用最少連接演算法可以使得nginx優先選擇連接最少的上游伺服器,需要用到upstream_least_conn模塊。如何跨worker進程生效因為nginx是多進程結構的,預設多個worker進程間無法共用數據。使用upstream_zone模塊可以實現這個需求。 ...
  • 前言: 原本計劃這次寫一下搭建eureka群集。但是發現上次寫的只是服務的註冊,忘了寫服務的發現,所以這次先把服務發現補上去。 需要using Pivotal.Discovery.Client; 這個時候發現名為order的有兩個服務。 然後再次啟動這三個.net core項目,並訪問http:// ...
  • AsnycLocal與ThreadLocal AsnyncLocal與ThreadLocal都是存儲線程上下文的變數,但是,在實際使用過程中兩者又有區別主要的表現在: AsyncLocal變數可以在父子線程中傳遞,創建子線程時父線程會將自己的AsyncLocal類型的上下文變數賦值到子線程中,但是, ...
  • 判斷節假日請求的Api:http://tool.bitefu.net/jiari/ /// <summary> /// 判斷是不是周末/節假日 /// </summary> /// <param name="date">日期</param> /// <returns>周末和節假日返回true,工作日 ...
  • 在學習Linux命令中,發現3個有關於文件內容操作的命令grep,sed和awk,在這裡簡單彙總這3個命令主要作用,在實際中找到最合適的情景應用,詳細用法可以參考其他文章。 1、grep命令 主要作用:用於搜索文件中特定字元串,並輸出整行內行 命令格式:grep [選項參數] "搜索內容" 文件(選 ...
  • Homebrew是mac上的軟體包管理工具,類似一些發行版Linux上的yum、apt get等。 安裝Homebrew 1、 安裝Xcode,到appstore搜索。 2、 打開終端,執行以下命令: 軟體安裝目錄 軟體都會安裝到 基本選項 安裝完成後,執行 ,就有提示 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...