閱讀989 返回首頁    go 阿裏雲 go 技術社區[雲棲]


100個開源C/C++項目中的bugs(一)數組和字符串處理的錯誤

from:https://www.oschina.net/question/1579_45444

100個開源C/C++項目中的bugs

摘要

本文演示靜態代碼分析的能力. 提供了100個已在開源C/C++項目中發現的錯誤例子給讀者研究。所有的錯誤已經被PVS-Studio靜態代碼分析工具發現。

介紹

我們不想讓你閱讀文檔感到厭倦,並馬上給你傳達錯誤例子.

那些想了解什麼是靜態代碼分析的讀者,請跟隨鏈接。想了解什麼是PVS-Studio的讀者,可下載試用版.請看該頁:https://www.viva64.com/en/pvs-studio/.

當然,還有一件事,請查看我們的帖子: "FAQ for those who have read our articles"

發現錯誤樣本的各個開源項目

錯誤實例將被歸為幾類。這種分法是很相對性的。 同一個錯誤常常同時屬於打印錯誤和不正確的數組操作錯誤.

當然我們僅從每個項目中列出少數幾個錯誤。如果我們描述所以已發現的錯誤,那將要編寫一本指南書了。以下是被分析的項目:

 數組和字符串處理的錯誤

數組和字符串處理的錯誤是C/C++程序中最大的一類。它以程序員可以高效處理低級內存問題為代價. 本文中,我們將展示一小部分被PVS-Studio分析器發現的此類錯誤。但我們相信任何C/C++程序員都了解他們有多巨大和陰險.

例 1. Wolfenstein 3D項目。對象僅被部分清除:

1 void CG_RegisterItemVisuals( int itemNum ) {
2   ...
3   itemInfo_t *itemInfo;
4   ...
5   memset( itemInfo, 0, sizeof( &itemInfo ) );
6   ...
7 }

The error was found through the V568 diagnostic: It's odd that the argument of sizeof() operator is the '&itemInfo' expression. cgame cg_weapons.c 1467.

sizeof() 操作符計算的是指針大小而非‘itemInfo_t’結構體大小。必須寫成"sizeof(*itemInfo)"。

例 2. Wolfenstein 3D項目。矩陣僅被部分清除:

1 ID_INLINE mat3_t::mat3_t( float src[ 3 ][ 3 ] ) {
2   memcpy( mat, src, sizeof( src ) );
3 }

The error was found through the V568 diagnostic: sizeof()操作返回的是指針的大小,非數組的大小,在'sizeof(src)'表達式中. Splines math_matrix.h 94.

通常開發者期望 'sizeof(src)' 操作返回"3*3*sizeof(float)"字節的數組大小。但根據語言規範,'src'僅是一個指針,而不是一個數組。因此,該矩陣隻被部分的複 製。‘'memcpy'’函數將拷貝4或8字節(指針大小),這依賴於代碼是32位還是64位的.

如果你希望整個矩陣都被拷貝,你可以給函數傳遞該數組的引用。以下是正確的代碼:

1 ID_INLINE mat3_t::mat3_t( float (&src)[3][3] )
2 {
3   memcpy( mat, src, sizeof( src ) );
4 }

例 3. FAR Manage 項目。數組僅被部分清除:

The error was found through the V579: memset 函數接收指針和它的大小作為參數. 這可能會引發錯誤。檢測第三個參數。far treelist.hpp 66.

最有可能的是, 計算待清除的元素數量的乘法操作丟失了, 而該代碼須如下所示:

"memset(Last, 0, LastCount * sizeof(*Last));".

例 4. ReactOS. 不正確的字符串長度計算.

01 static const PCHAR Nv11Board = "NV11 (GeForce2) Board";
02 static const PCHAR Nv11Chip = "Chip Rev B2";
03 static const PCHAR Nv11Vendor = "NVidia Corporation";
04  
05 BOOLEAN
06 IsVesaBiosOk(...)
07 {
08   ...
09   if (!(strncmp(Vendor, Nv11Vendor, sizeof(Nv11Vendor))) &&
10       !(strncmp(Product, Nv11Board, sizeof(Nv11Board))) &&
11       !(strncmp(Revision, Nv11Chip, sizeof(Nv11Chip))) &&
12       (OemRevision == 0x311))
13   ...
14 }

該錯誤經由V579診斷:strncmp 函數接收了指針和它的大小做為參數。這可能是個錯誤。查看第三個參數。vga vbe.c 57

'strncmp'的函數的調用僅僅比較了前幾個字符,而不是整個字符串。這裏的錯誤是:sizeof()操作,在這種情況下用來計算字符串長度絕對不適宜。sizeof()操作實際上隻計算了指針的大小而不是string的字節數量。

關於該錯誤最討厭和陰險的是,該代碼大多數時候都如預期的工作。99%的情況下,比較前幾個字符就足夠了。但剩下的1%能帶給你愉快和長時間的調試過程。 

例 5. VirtualDub 項目. 數據越界(明確的下標).

01 struct ConvoluteFilterData {
02  long m[9];
03  long bias;
04  void *dyna_func;
05  DWORD dyna_size;
06  DWORD dyna_old_protect;
07  BOOL fClip;
08 };
09  
10 static unsigned long __fastcall do_conv(
11   unsigned long *data,
12   const ConvoluteFilterData *cfd,
13   long sflags, long pit)
14 {
15   long rt0=cfd->m[9], gt0=cfd->m[9], bt0=cfd->m[9];
16   ...
17 }

The code was found through the V557 diagnostic: 數組可能越界。下標‘9’已經指向數組邊界外. VirtualDub f_convolute.cpp 73

這不是一個真正的錯誤,但是個好的診斷。解釋: https://www.viva64.com/go.php?url=756

例 6. CPU Identifying Tool 項目. 數組越界(宏中的下標)

01 #define FINDBUFFLEN 64  // Max buffer find/replace size
02 ...
03 int WINAPI Sticky (...)
04 {
05   ...
06   static char findWhat[FINDBUFFLEN] = {'\0'};
07   ...
08   findWhat[FINDBUFFLEN] = '\0';
09   ...
10 }

The error was found through the V557 diagnostic:數組可能越界.下標‘64’已經指向數組邊界外。stickies stickies.cpp 7947

該錯誤與上例類似。末端 null 已被寫到數組外。正確代碼是:"findWhat[FINDBUFFLEN - 1] = '\0';". 

例 7. Wolfenstein 3D 項目. 數組越界(不正確的表達式).

01 typedef struct bot_state_s
02 {
03   ...
04   char teamleader[32]; //netname of the team leader
05   ...
06 }  bot_state_t;
07  
08 void BotTeamAI( bot_state_t *bs ) {
09   ...
10   bs->teamleader[sizeof( bs->teamleader )] = '\0';
11   ...
12 }

The error was found through the V557 diagnostic: 數組可能越界. 'sizeof (bs->teamleader)' 下標已指向數組邊界外。game ai_team.c 548

這是又一個數組越界的例子, 當使用了明確聲明的下標時候. 這些例子說明了,這些不起眼的錯誤比看上去更廣泛.

末端的 null 被寫到了 'teamleader' 數組的外部。下麵是正確代碼:

1 bs->teamleader[
2  sizeof(bs->teamleader) / sizeof(bs->teamleader[0]) - 1
3  ] = '\0';

Example 8. Miranda IM 項目. 僅字符串的部分被拷貝.

01 typedef struct _textrangew
02 {
03   CHARRANGE chrg;
04   LPWSTR lpstrText;
05 } TEXTRANGEW;
06  
07 const wchar_t* Utils::extractURLFromRichEdit(...)
08 {
09   ...
10   ::CopyMemory(tr.lpstrText, L"mailto:", 7);
11   ...
12 }

The error was found through the V512 diagnostic: 一個 'memcpy' 函數的調用將導致緩衝區上溢或下溢。tabsrmm utils.cpp 1080

如果使用Unicode字符集,一個字符占用2或4字節(依賴於編譯器使用的數據模型)而不是一字節。不幸的是,程序員們很容易忘記,你常常會在程序中看到類似該例子的汙點。

'CopyMemory' 函數將隻拷貝L"mailto:"字符串的部分,因為他處理字節,而不是字符。你可以使用一個更恰當的字符串拷貝函數修複該代碼,或者,至少是,將 sizeof(wchar_t) 與 數字7 相乘.

例 9. CMake 項目. 循環中的數組越界.

01 static const struct {
02   DWORD   winerr;
03   int     doserr;
04 } doserrors[] =
05 {
06   ...
07 };
08  
09 static void
10 la_dosmaperr(unsigned long e)
11 {
12   ...
13   for (i = 0; i < sizeof(doserrors); i++)
14   {
15     if (doserrors[i].winerr == e)
16     {
17       errno = doserrors[i].doserr;
18       return;
19     }
20   }
21   ...
22 }

The error was found through the V557 diagnostic: 數組可能越界.'i'下標值可到達 367. cmlibarchive archive_windows.c 1140, 1142

該錯誤處理本身就包含了錯誤。sizeof() 操作符以字節數返回數組大小而不是裏麵的元素的數量。因此,該程序將在循環中試圖搜索多於本應搜索的元素.下麵是正確的循環:

1 for (i = 0; i < sizeof(doserrors) / sizeof(*doserrors); i++)

例 10. CPU Identifying Tool 項目. A string is printed into itself.

01 char * OSDetection ()
02 {
03   ...
04   sprintf(szOperatingSystem,
05           "%sversion %d.%d %s (Build %d)",
06           szOperatingSystem,
07           osvi.dwMajorVersion,
08           osvi.dwMinorVersion,
09           osvi.szCSDVersion,
10           osvi.dwBuildNumber & 0xFFFF);
11   ...
12   sprintf (szOperatingSystem, "%s%s(Build %d)",
13            szOperatingSystem, osvi.szCSDVersion,
14            osvi.dwBuildNumber & 0xFFFF);
15   ...
16 }

This error was found through the V541 diagnostic: It is dangerous to print the string 'szOperatingSystem' into itself. stickies camel.cpp 572, 603

一個格式化字符串打印到自身的企圖,可能會導致不良後果。

代碼的執行結果依賴於輸入數據,結果未知。可能的,結果會是一個無意義字符串或一個 Access Violation 將發生。

該錯誤可以歸為 "脆弱代碼" 一類.在某些程序中,向代碼提供特殊字符,你可以利用這些代碼片段觸發緩衝區溢出或者被入侵者利用.

例 11. FCE Ultra 項目。某字符串未獲得足夠內存

1 int FCEUI_SetCheat(...)
2 {
3   ...
4   if((t=(char *)realloc(next->name,strlen(name+1))))
5   ...
6 }

The error was found through the V518 diagnostic: 'realloc' 函數通過 'strlen(expr)' 申請了數量奇怪的內存. 可能正確的變體是 'strlen(expr) + 1'. fceux cheat.cpp 609

該錯誤由一個錯誤打印導致。strlen() 函數的參數必須是 "name" 指針而不是 "name+1" 指針。因此,realloc 函數比實際需要少申請了兩個字節:1個字節丟失了,因為1沒有被加入到字符串長度中。另一個字節也丟失了,因為'strlen'函數計算長度時跳過了第一 個字符。

例 12. Notepad++ 項目。部分的數組清理。

1 #define CONT_MAP_MAX 50
2 int _iContMap[CONT_MAP_MAX];
3 ...
4 DockingManager::DockingManager()
5 {
6   ...
7   memset(_iContMap, -1, CONT_MAP_MAX);
8   ...
9 }

The error was found through the V512 diagnostic:memset 函數的調用將導致緩衝區上溢或下溢. notepadPlus DockingManager.cpp 60

這是又一個數組元素數量與數組大小混淆的例子。一個通過 sizeof(int) 的乘法操作丟失了。

我們可以繼續給你指出更多我們在各種項目中發現的數組處理錯誤例子。但我們不得不停止了。 

最後更新:2017-04-02 22:16:38

  上一篇:go 設計模式——簡單工廠模式
  下一篇:go Spring-Bean的銷毀使用destroy-method()方法無效解決方案(容器!附源碼)