blog 文章

2025年10月10日 星期五

linux qe editor 與 -O2 double free issue

The most difficult bugs to fix are the ones that don’t exist.
qe 是類似 dos pe2 或是漢書的文字編輯器。在 google 搜尋已經找不到這個軟體的資訊, 所以想寫一篇來紀錄。

原作者是 Jiann-Ching Liu, https://github.com/descent/qe 忘記從那個地方 clone 來, readme 提到的網址 http://www.cc.ncu.edu.tw/~center5/product/qe/ 已經無法連上, 所以只貼我 clone 的 source code。

readme 提到的網站 http://www.cc.ncu.edu.tw/~center5/product/qe/ 當然已經連不上。

按下2次 ESC, 會切到下方的命令列, 打 quit 可以離開 qe。在剛從 dos 轉到 linux 時, 還不太會用 vi 時, 短暫用過 qe。

目前的版本在 gcc 14 編譯會有 double free 的問題, 無法正常執行, 我嚇傻了, 之前用還好好的, 加上 sanitizer -fsanitize=undefined -fsanitize=address 後可以正常執行, 參考 fig 1。

身為軟體工程師, 看到這棘手的問題, 很想找出問題, 用了 valgrind, sanitizer 來查 double free, 不過沒什麼進展。由於是用 ncurses 寫的, 在除錯印出 debug message 上會有點麻煩, 增加點除錯困擾。

追了一下 code, 這應該是早期的 c++, 還沒有容器和 std::string, 要不然應該不會自己寫 linked list 和 string。

fig 1. qe

dirbuffer.cc
180 int dirbuffer::refreshdir(const char *path) {
183     linebuffer            *tmpptr, *ptr;
208     sprintf(linebuf, "[ %s ]", pathname.getString());
209     filename = linebuf;
210
211     if ((dirp = opendir(pathname.getString())) != NULL) {
212
213      for (ptr = head->next; ptr != tail; ptr = current) {
214          current = ptr->next;
215          ptr->~linebuffer(); // 引發 double free
216          delete ptr;
217      }

找了很久, 本來以為是 linebuffer 的 linked list 出問題, 結果是 qeString 引發, 並不是 linebuffer 這個 class, qeString 是類似 std::string 的東西, 我把動態 malloc 改成固定 array。
char         str[10000];
就解決這個問題。
 class qeString {
 protected:
-    char        *str;
+    char        str[10000];
有點奇怪, 感覺還是沒找到關鍵問題。

20251014 終於找到原因了, 的確和 qeString 有關。qe.cc 是簡化的版本, 這個就會得到 free(): double free detected in tcache 2 錯誤訊息。

qe.cc
 1 int main(int argc, char *argv[])
 2 {
 3     linebuffer        *head, *tail, *current;
 4
 5     head    = new linebuffer("=== Top of file ===");
 6     tail    = new linebuffer("=== Bottom of file ===");
 7     current = new linebuffer("");
 8     head->previous     = NULL;
 9     head->next         = current;
10     current->previous  = head;
11     current->next      = tail;
12     tail->previous     = current;
13     tail->next         = NULL;
14
15     current->~linebuffer();
16     printf("current: %p\n", current);
17     delete current;
18 }

原因是 qe.cc L15, linebuffer 繼承 qeString, 當手動喚起 linebuffer 解構函式時, 一併發動 qeString::~qeString(), 會 delete [] str, 而 qe.cc L17, delete current 會再次發動 qeString::~qeString(), 所以又再一次 delete [] str, 造成 double free。
delete current; 其實就會執行 current->~linebuffer();
是不是早期 c++ delete current 不會執行 current->~linebuffer();
qestring.cc
1 qeString::~qeString(void) {
2     if (str != NULL) delete [] str;
3     str = NULL;
4     buflen = len = 0;
5 }

參考 qe_df.log L9, L12, str 被重複 delete。

list 5. qe_df.log
 1 descent@deb64:qe$ ./qe
 2 init s: === Top of file ===, slen: 19
 3 str: 0x5595b8b29700
 4 init s: === Bottom of file ===, slen: 22
 5 str: 0x5595b8b29750
 6 init s: , slen: 0
 7 str: 0x5595b8b297a0
 8 next: 0x5595b8b29720, previous: 0x5595b8b292c0
 9 ~ str: 0x5595b8b297a0
10 current: 0x5595b8b29770
11 next: (nil), previous: (nil)
12 ~ str: 0x5595b8b297a0
13 free(): double free detected in tcache 2
14 Aborted

可是為什麼會這樣, qestring.cc L3 在 delete 之後有把 str 設定為 NULL, 照理來說 delete NULL 是不會有問題的, 後來發現是 -O2 影響的, 把 -O2 拿掉就正常。

list 6. no -O2
 1 descent@deb64:qe$ ./qe
 2 init s: === Top of file ===, slen: 19
 3 str: 0x55bd3ec88700
 4 init s: === Bottom of file ===, slen: 22
 5 str: 0x55bd3ec88750
 6 init s: , slen: 0
 7 str: 0x55bd3ec887a0
 8 next: 0x55bd3ec88720, previous: 0x55bd3ec882c0
 9 ~ str: 0x55bd3ec887a0
10 current: 0x55bd3ec88770
11 next: (nil), previous: (nil)
12 ~ str: (nil)
13 next: 0x55bd3ec88770, previous: (nil)
14 ~ str: 0x55bd3ec88700
15 next: (nil), previous: 0x55bd3ec88770
16 ~ str: 0x55bd3ec88750

list 6 沒有 -O2, 就沒遇到 delete 同個位址 str 的問題, 看來是 -O2 最佳化引起的。

gcc 7 也會編譯出有問題的 code, 一樣要拿掉 -O2。

list 7, 比較有無 -O2 差異
 1 descent@deb64:qe$ ./qe
 2 init s: === Top of file ===, slen: 19
 3 str: 0x55bd3ec88700
 4 init s: === Bottom of file ===, slen: 22
 5 str: 0x55bd3ec88750
 6 init s: , slen: 0
 7 str: 0x55bd3ec887a0
 8 next: 0x55bd3ec88720, previous: 0x55bd3ec882c0
 9 ~ str: 0x55bd3ec887a0
10 current: 0x55bd3ec88770
11 next: (nil), previous: (nil)
12 ~ str: (nil)
13 next: 0x55bd3ec88770, previous: (nil)
14 ~ str: 0x55bd3ec88700
15 next: (nil), previous: 0x55bd3ec88770
16 ~ str: 0x55bd3ec88750
17 
18 no -O2, 觸發 4次 ~str, 
19 
20 descent@deb64:qe$ ./qe
21 init s: === Top of file ===, slen: 19
22 str: 0x55b33169b700
23 init s: === Bottom of file ===, slen: 22
24 str: 0x55b33169b750
25 init s: , slen: 0
26 str: 0x55b33169b7a0
27 next: 0x55b33169b720, previous: 0x55b33169b2c0
28 ~ str: 0x55b33169b7a0
29 current: 0x55b33169b770
30 next: (nil), previous: (nil)
31 ~ str: 0x55b33169b7a0
32 free(): double free detected in tcache 2
33
34 -O2 觸發 2 次 ~str, 的確有最佳化,只是最佳化錯了, delete L28, L31 同樣位址

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。