Try   HackMD

2019q1 Homework5 (daemon)

contributed by < flawless0714 >
Kernel 版本: 4.15.0-47-generic

筆記

TCP backlog

原先擔心 kernel module 中的這項參數會影響測試,理解後得知其是在 accept() 成功前 queue 中最大能存放的連線要求,然而我的測試程式是在 accept() (以測試程式的角度看是 connect()) 成功後才會開始計時,所以這邊除非 queue 太容易塞滿,導致測試程式的部份執行緒沒有被涵蓋到 kecho 並行處理通訊時的情境。

kthread

工作於 kernel-space 的 thread。

kthread_run()

建立新 kthread 並開始執行,回傳值為 task_struct

kthread_stop()

通知 kthread 準備關閉(將 kernel_should_stop 旗標設為 true),並等待直到 threadfn 結束(由此可知 threadfn 中需要有一直驗證停止旗標的實做)。其中會把 kthread->kthread_should_stop 設為 true。此函數也用於喚醒 kthread。

排程

當 kthread 閒置時需要將其 block,以免發生浪費資源的情況。常見的 block 方法為使用 schedule()ssleep()

schedule() 睡眠之運作方式

首先使用 set_current_state() 將 kthread 的 state 改成 TASK_INTERRUPTIBLE,以使得待會可以被移出 run queue (排程器排程時會從 run queue 挑 task),接著呼叫 schedule(),告知排程器可以切到其他 task 了,也就是讓出 CPU 資源,至此這個 kthread 即開始睡眠。假如要喚醒這個 kthread,我們可以使用 wake_up_process() (帶入參數為目標 kthread 的 task_struct*),這個函數會把 kthread 的 state 切回 TASK_RUNNING ,並將其它放回 run queue,接著就等待排程器下一次排程了。

kecho

運作流程

  1. 初始化時,進行 server socket 的 create, bindlisten 等 socket 設定。
  2. 開始 listen 後,建立第一個 kthread,這個執行緒為 server daemon,其負責進行 listen,每當有新進連線時,它會開新的執行緒(server worker)來 handle 新連線。
  3. server worker 會先呼叫 get_response 來等待 client 端傳入訊息。
  4. 收到來自 client 的訊息後隨即呼叫 send_request 以回傳 client 傳入之訊息,以達到 echo 訊息的功能。
  5. 重複第四步直到 client 關閉連線(至此 server worker 這個執行緒將被關閉)或 kecho 模組被移除(rmmod)。

問題

  • server worker (kthread) 是如何在 client 端關閉連線時得知需要把自己關掉?

由不斷 evaluate kthread_should_stop 回傳的布林值去判斷是否需要關閉。

  • 為什麼 detachstate 為 JOINABLE (使用 pthread_attr_getdetachstate 確認過) 的 pthread 可以在 pthread_join 結束後(檢查過 retval 正常)才結束?

已知問題

  • 當輸入資料量大到一定量時,會回傳亂碼,例如 / 下的檔案名稱,還有出現過自己的 kernel cmdline

    多次測試後發現就算輸入資料很小也會發生回傳檔名,而且是發生在 get_response 的時候,所以可以說是 telnet 自己傳過去的,或是 kmalloc 配置的記憶體沒有初始化的關係(首次 echo 即發生此問題),待測試。

    看來是 kmalloc 配置的記憶體沒初始化造成的,剛剛測試配置完馬上用 memset 初始化記憶體,就沒再遇到這問題了,不然每次 telnet 第一次連線就算你直接按 enter 鍵送空字串, kecho 也會回傳一串亂碼。不知道這算不算漏洞,連 cmdline (/proc/cmdline) 都露出來了,這樣可能整個 / 下的檔名都可用這招掏出來

    想想覺得這應該不算漏洞,因為這個 module 算是沒有經過驗證之類的檢查,而且載入後 kernel 就變成 tainted kernel 了。

    • 修復:
      如上述,只需於使用 buf 前初始化記憶體即可。

      echo_server_worker:

      ​​​​​​​​buf = kmalloc(BUF_SIZE, GFP_KERNEL);
      ​​​​​​​​if (!buf) {
      ​​​​​​​​    printk(KERN_ERR MODULE_NAME ": kmalloc error....\n");
      ​​​​​​​​    return -1;
      ​​​​​​​​}
      ​​​​
      ​​​​​​​​memset(buf, 0, BUF_SIZE);
      
  • msg buffer 沒有重置,進而導致每筆新訊息會與前一筆訊息重疊(前提是當前訊息長度小於前一次,否則新訊息會完全覆蓋舊訊息)。

    • 修復:
      透過在每次 echo 結束後重置 buffer,須注意由於 kthread 在關閉前還會執行一套接收(get/send _request),而由於我們有做清空的動作,所以進行最後一次 send_request 時帶入的 size 會是0,這會讓 send_request 中的 kernel_sendmsg 帶入的 size 參數拿到18446744073709551615(以 size_t 來計算 0 - 1),進而造成 kernel_sendmsg 執行時發生 kernel panic (null dereference at 000000000000)。話說這是個有趣的故事的開始,遇到 panic 後我想說這個 size 對記憶體位置來說已經是天文數字了,於是就試試小一點的,我直接把 size 給定 BUF_SIZE + 1000,想看看 panic log 會說我想存取哪段記憶體,答案是 null dereference at 0000000013E8,看到這邊還稍有意義(13E8 轉為十進制是5096),再接著我又好奇假如放248(x86_64 最大定址位置)會發生啥事,結果還沒測到248-1,只是先測248就差點把我的 / 搞爆了,會這麼說是因為我開不了機,只能用 ctrl + fx 進 TTY 而開不了視窗桌面,而且原本的密碼不能登入..,然後進 TTY 後還跳出 kdump 無法初始化的錯誤,並且提示 / 已無可用空間..,結果 df -h 一看發現 / 被用光了(原本還有 4GB 左右),實在不懂這是怎麼造成的,等虛擬機有成功打開 kdump 再來測試看看248-1會發生什麼事,不然怕下次我的實體機沒這麼好運還可以開機

      echo_server_worker():

      ​​​​​​​​while (!kthread_should_stop()) {
      ​​​​​​​​    res = get_request(sock, buf, BUF_SIZE - 1);
      ​​​​​​​​    if (res <= 0) {
      ​​​​​​​​        if (res) {
      ​​​​​​​​            printk(KERN_ERR MODULE_NAME ": get request error = %d\n", res);
      ​​​​​​​​        }
      ​​​​​​​​        break;
      ​​​​​​​​    }
      
      ​​​​​​​​    res = send_request(sock, buf, strlen(buf));
      ​​​​​​​​    if (res < 0) {
      ​​​​​​​​        printk(KERN_ERR MODULE_NAME ": send request error = %d\n", res);
      ​​​​​​​​        break;
      ​​​​​​​​    }
      
      ​​​​​​​​    memset(buf, 0, BUF_SIZE);
      ​​​​​​​​    }
      

      send_request():

      ​​​​​​​​length = kernel_sendmsg(sock, &msg, &vec, 1, (size < BUF_SIZE) ? size : 0);
      

效能分析

測試用之 client 端程式碼

  • 已知問題
    • (已改用 gettimeofday)數據出現負號。發生原因為平均數量級差異過大造成溢位,故須捨棄 ns 級的時間量測 API,改用更粗糙的單位。
    • (已解決)不該於 worker 中使用 while(flag); 去等待其他 thread 的建立(等1000個 thread 的實驗已經等了至少20分鐘了,最後單次傳輸超過一秒被強制停止。推測發生原因是這支 process 的資源被那些已建立的 thread 拿去卡在 while,所以後續建立的 thread 其實沒什麼資源可以用)。

      修好後不只問題解決,連 concurrent thread 有數百個時 CPU 也不會有太大的負載(原本是全部核心使用率都接近100%)。

    • (已解決)若未於 pthread_join 後使用 usleep 稍待一會再開始下一輪(concurrent thraed 數量相同,此舉之目的為取平均時間)測試,則 thread 用來索引量測時間陣列的變數(idx)會發生多次 idx++ 的問題,進而導致 OOB access of array。目前已經確認(手動使用 pthread_attr_init 去初始化 attr,再使用 pthread_attr_getdetachstate 去檢驗)過 pthread_join 使用的預設 attr 中的 detachstateJOINABLE 的(雖然 manpage 已經有說這是預設了,但我快找不到問題點了QQ,所以還是檢查一下)

      剛剛測試又發現新情境,在沒有使用 usleep 的情況下若從初始數量大一點(e.g. 500)的 concurrent thread 開始測試即不會發生上述的 OOB 問題,問題越來越深了..。

      找到問題了,是我太蠢,我把 cur_thread_cnt 的初始值設0,這造成 pthread_join 的迴圈在 cur_thread_cnt 為 0 (其實是單個 thraed) 時連進都進不了,因為我的迴圈設定是 for (int x = 0; x < cur_thread_cnt; x++)

原始版(未使用 cmwq)


完全不可考,晚點更新修正版本。(此版本在 thread function 中使用 while 迴圈來等待一個 global 的旗標,此舉耗費非常多資源在單個 thread 上面)

[修正版] (添加 pthread_cond_wait 以節省運算資源之浪費)


由圖中可發現通訊時間平均花費了 10ms 左右

cmwq 修改版
整合 Kernel Httpd 版

注意事項

  • (已排除, )關閉 telnet 時建議使用 close or q command 關閉連線,若用 ctrl + z,會讓未關閉的連線一睡不起 (suspend),進而造成下一次 insmod 時發生位置佔用的問題(address already in use),目前解法為重開機。可用下方 command 來查看哪些 port 正在使用:
    ​​​​sudo netstat -apn |grep {port (e.g. 12345)}
    

參考資料

tags: linux2019