作者:John Regehr
原作:http://blog.regehr.org/archives/226
當像邊界檢查GCC,Purify,Valgrind這樣的工具第一次出現時,在它們下面運行任意一個UNIX應用程式是有趣的。檢查器的輸出顯示這些應用程式,儘管工作得很好,執行了大量的記憶體安全性錯誤,比如使用未初始化資料、數組訪問越界等。僅運行grep或不管什麼將導致數以十計或百計的這些錯誤發生。
發生了什嗎?基本上,C/UNIX執行環境的附帶屬性使得這些錯誤(通常)是溫和的。例如,由malloc()返回的塊通常在之前及/或之後包含一些填充位元組;這些填充位元組可以吸收越界儲存,只要它們不是離分配地區太遠。值得消除這些bug嗎?是的。首先,一個帶有不同屬性的執行環境,比如一個提供減少填充位元組的嵌入系統的malloc(),會把溫和的近失(near-miss)數組寫變為兇險的堆訛誤bug。其次,在不同的情形下,同一個溫和的bug甚至可能,在同一個執行環境裡,導致一個崩潰或訛誤。開發人員通常發現這些類型的爭論是令人信服的,目前大多數UNIX程式是相對Valgrind淨化的。
用於尋找整數未定義行為的工具不像記憶體不安全性檢查器那麼成熟。在C及C++中壞的整數行為包括有符號數溢出、除0、位移超出位元寬度等。近年來,這些成為了更加嚴重的問題,因為:
- 整數缺陷是嚴重安全問題的一個來源
- 在利用整數未定義行為來產生高效代碼方面,C編譯器已經變得大為激進
最近我的學生Peng Li實現了一個整數未定義行為的檢查工具。使用它,我們發現許多程式包含這些bug。例如,超過半數的SPECINT2006基準測試執行了或這或那的整數未定義行為。今天在許多方面,整數bug的情形與1995年左右記憶體bug的情形類似。說得更明確點,整數檢查工具確實存在,但看起來它們沒有廣泛使用,而且它們中的許多工作在2機制檔案上,太晚了。在編譯器有機會利用未定義行為之前,你必須看一下原始碼——然後消除它。
本貼的餘下部分探討我們在LLVM:一個中等大小(~800KLOC)的開源C++程式碼程式庫中發現的幾個整數未定義行為。當然我這裡不是挑剔LLVM:它是品質非常高的代碼。想法是通過看一下,在這個經過良好測試的代碼中,未檢測出的、潛藏的某些問題,我們可以有望學會在將來然後避免寫出這些bug。
作為一個無目的的註解(asa random note),如果我們把LLVM代碼視為C++0x而不是C++98,那麼會出現大量額外的位移相關的未定義行為。在後續貼中我將談及新的位移限制(它等同於C99中的限制)。
我稍微整理了工具的輸出以提高可讀性。
整數溢出#1
錯誤訊息:
UNDEFINED at <BitcodeWriter.cpp, (740:29)> :
Operator: -
Reason: Signed Subtraction Overflow
left (int64): 0
right (int64): -9223372036854775808
代碼:
int64_t V = IV->getSExtValue();
if (V >= 0)
Record.push_back(V << 1);
else
Record.push_back((-V << 1) | 1); <<----- bad line
在運行在2進位編碼機器上的所有現代C/C++變種中,對值是INT_MIN(或在這個情形裡,INT64_MIN)的int求負是未定義的行為。對這個情形,修改是添加一個顯式的檢查。
編譯器會利用這個未定義的行為嗎?它們會:
[regehr@gamow ~]$ cat negate.c
int foo (int x) __attribute__ ((noinline));
int foo (int x)
{
if (x < 0) x = -x;
return x >= 0;
}
#include <limits.h>
#include <stdio.h>
int main (void)
{
printf (“%d\n”, -INT_MIN);
printf (“%d\n”, foo(INT_MIN));
return 0;
}
[regehr@gamow ~]$ gcc -O2 negate.c -o negate
negate.c: In function ‘main’:
negate.c:13:19: warning: integer overflow in expression [-Woverflow]
[regehr@gamow ~]$ ./negate
-2147483648
1
在C編譯器有矛盾的想法中,-INT_MIN即是負的又是非負的。如果第一個真正的AI(譯註:人工智慧)以C或C++編寫,我想它會立即推匯出自由即是奴役、愛即是恨、和平即是戰爭。
整數溢出#2
錯誤訊息:
UNDEFINED at <InitPreprocessor.cpp, (173:39)> :
Operator: -
Reason: Signed Subtraction Overflow
left (int64): -9223372036854775808
right (int64): 1
代碼:
MaxVal = (1LL << (TypeWidth – 1)) – 1;
在C/C++中,像這樣計算最大有符號整數值是非法的。有更好的方式,比如建立一個全1的向量,然後清除高位的位元。
整數溢出#3
錯誤訊息:
UNDEFINED at <TargetData.cpp, (629:28)> :
Operator: *
Reason: Signed Multiplication Overflow
left (int64): 142998016075267841
right (int64): 129
代碼:
Result += arrayIdx * (int64_t)getTypeAllocSize(Ty);
這裡分配的大小是貌似合理的,但對於任何可想象的數組,數組索引超出了邊界。
位移超出位元寬度#1
錯誤訊息:
UNDEFINED at <InstCombineCalls.cpp, (105:23)> :
Operator: <<
Reason: Unsigned Left Shift Error: Right operand is negative or is greater than or equal to the width of the promoted left operand
left (uint32): 1
right (uint32): 63
代碼:
unsigned Align = 1u << std::min(BitWidth – 1, TrailZ);
這完全是一個bug:BitWidth被設定為64,但應該是32。
位移超出位元寬度#2
錯誤訊息:
UNDEFINED at <Instructions.h, (233:15)> :
Operator: <<
Reason: Signed Left Shift Error: Right operand is negative or is greater than or equal to the width of the promoted left operand
left (int32): 1
right (int32): 32
代碼:
return (1 << (getSubclassDataFromInstruction() >> 1)) >> 1;
當getSubclassDataFromInstruction()返回在範圍128-131內的一個值時,左移的右實參值為32(譯註:上面的代碼應是getSubclassDataFromInstruction()>> 2)。(在任一方向)移動這個位元寬度或更大是一個錯誤,因此這個函數要求getSubclassDataFromInstruction()返回不大於127的值。
結論
使得某些程式行動錯誤,但沒有給開發人員任何方式來告知他們的代碼是否執行這些行動,如果是在何處,基本上是邪惡的。C的設計點之一是“相信程式員”。這很好,但有信心然後才有信任(there’strust and then there’s trust)。我的意思是,我信任我5歲的孩子,但我仍然不會讓他獨自穿過一條繁忙的街道。以C或C++建立一大段安全性關鍵或保密性關鍵的代碼,在編程方面等同於蒙上眼睛穿越8車道高速公路。