from: http://www.51testing.com/?uid-10851-action-viewspace-itemid-73347
1 FORWARD_NULL
通常发生的情况是,一个指针先被判断是否等于NULL,然后指针被非法引用。
非法引用NULL的指针会导致程序崩溃。程序员在判断指针是否等于NULL时,没能正确的处理好,或者是忘记了NULL在代码路径的情况。
举例说明:
int forward_null_example1(int *p) { int x; if( p == NULL ) { x = 0; } else { x = *p; } x += fn(); *p = x; // ERROR: p is potentially NULL return 0; }
2 USE_AFTER_FREE
即使用已被释放的内存、同一指针被释放多次。
当内存被释放后,就不能再被安全的使用。而同一内存的多次释放通常会导致未定义的行为,包括内存冲突和程序崩溃。
引用已释放的指针也是非常危险的,因为指针的值不确定或者指针指向任意内存的位置。
在多线程的程序中,多次释放同一内存非常危险,因为已释放的内存可能已被另一线程申请,再次释放可能导致该线程的内存也被释放,在这种情况下,该线程使用已释放的内存,可能导致紊乱,同时很难被编译器跟踪。
举例说明:
void fun(int * p) { free (p); int k = *p; // p is dereferenced after it is freed. } int f(void *p) { if(some_error()) { free(p); return -1; } return 0; } void g() { void *p = malloc(42); if(f(p) < 0) { free(p); // double free of pointer "p" // previously freed in call to "f". } use(p); }
3 RESOURCE_LEAK
资源泄漏(内存泄漏)指变量在出了自己的作用范围后,占用的资源仍然驻守在内存里,没有被释放。
严重的内存泄漏能导致进程崩溃,即便是很小的内存泄漏,在系统长时间运行没有重启后,也会产生错误。如果内存泄漏是由用户输入或者网络数据触发,还会成为“拒绝服务攻击”的对象。
文件句柄或者网络套接字的泄漏会导致程序崩溃、拒绝服务攻击或者打开其他文件或套接字失败。操作系统通常会限制进程的文件句柄和套接字个数。当达到限制的最大值时,进程要申请新的资源时,首先要关闭一些已打开的资源。如果进程中存在资源泄漏,进程自己将没有办法回收这些资源,除非强行终止该进程。多数情况下,这些泄漏通常发生在某个错误的路径,比如说,某个异常处理的分支。这种情况下,正确的做法应该是将程序跳转(GOTO)到该函数的出口,出口处应释放这些资源。
在C++中,RAII(the Resource Acquisition Is Initialization)机制能够自动的释放资源。RAII包括类的一个申请资源的构造函数和释放资源的析构函数。当一个类的局部变量声明后,当离开该变量的作用范围后,RAII会自动调用其析构函数释放资源,这同样也会保护throw异常导致的泄漏。
举例说明:
int leak_example(int c) { void *p = malloc(10); if(c) { return -1; // "p" is leaked } /* ... */ free(p); return 0; } int wrong_error_check() { void *p = malloc(10); void *q = malloc(20); if(!p || !q) { return -1; // "p" or "q" may be leaked if the other is NULL } /*...*/ free(q); free(p); return 0; } int test(int i) { void *p = malloc(10); void *q = malloc(4); if(i > 0) { p = q; // p is overwritten and is the last pointer } else // to the allocated memory { free(q); } free(p); return 0; } void test(int c) { FILE *p = fopen("foo.c", "rb"); if(c) { return; // leaking file pointer "p" } fclose(p); }
4 NULL_RETURN
函数的返回值可能是NULL,所以使用函数返回值的变量一定要首先检查是否是NULL,否则就可能出错。
程序员经常不会去检查函数的返回值,而是直接以危险的方式去使用,可能会由于对NULL的非法引用,导致程序崩溃。
举例:
void bad_malloc() { // malloc returns NULL on error struct some_struct *x = malloc(sizeof(*x), GFP_KERNEL); // ERROR: memset dereferences x memset(x, 0, sizeof(struct some_struct); }
5 RETURN_LOCAL
函数的返回值是局部变量(堆栈)的地址。
在C/C++语言中,当被调用函数执行完毕,其所有的局部变量就不再有效,它们的内存区域可能会被新的调用中的变量所覆盖。
所以,指向局部(堆栈)变量的指针不应该被返回,否则将会引起内存冲突和未明确的行为。
例子请参考blog上的另外一篇文章《一个“中软”的关于内存的笔试题》的第二题。
6 UNINIT
变量在使用前没有初始化。
如果没有初始化的话,堆栈中的变量没有固定的数值。使用了没有初始化的变量会导致不可预测的行为,或者安全漏洞。
举例说明:
int uninit_example1(int c) { int x; if(c) { return c; } else { return x; // "x" is not initialized } } int result; int uninit_example2(int c) { int *x; if(c) { x = &c; } use (x); // uninitialized variable "x" and "*x" used in call } void use (int *x) { result = *x+2; }
7 MISSING_RETURN
在返回类型为非void的函数中,值没有被返回。比较常见的情况是,在不同的分支要返回不同的值,结果有一个分支漏了return。
举例:
int fn(int x) { switch (x) { case 5: return 4; default: return 5; } // no return; but not a defect, since unreachable }
8 INFINITE_LOOP
程序中有不能终止的循环,会导致程序挂起或者崩溃。当然在多任务的程序中,故意设计的死循环除外。
9 DEADCODE
由于条件判断总是TRUE或FALSE,导致一个分支内的代码永远执行不到,这些永远都执行不到的代码称为“死代码”。
表面看,死代码好像不是什么大问题,无非就是让代码规模大了一点;但反过来想,正常情况下,没有哪个程序员去故意写“死代码”,
即便是防御性的异常处理代码,都应该是有可能执行到的。所以说,程序中存在“死代码”一般是由逻辑错误引起的。
在情况严重时,这些逻辑错误可能导致重要的代码永远不能执行,造成严重的后果。
举例说明:
int deadcode_example2(int array[10]) { int i; int value = -1; for( i = 0; i < 10; i++ ) { // ERROR: increment is unreachable; array // not properly searched if( array[i] > 100 ) // the break below probably meant to be { value = array[i]; // part of the true branch of the if statement } break; // but there are no braces here } return value; }
10 OVERRUN_STATIC
主要是指数组或者指针的索引为常量情况下的越界。
最常见的安全漏洞是“缓冲区溢出(buffer overrun)”。缓冲区溢出可以导致内存访问超出边界,导致内存冲突,造成很难定位的安全漏洞,黑客有可能通过该漏洞控制系统。
举例说明:
void overrun_pointer() { int buf[10]; // buff has 10 elements, index 0 to 9 int *x = &buff[1]; // x now points to buff[1] x[9] = 0; // x[9] is equivalent to buff[10], // which is out of bounds }
11 OVERRUN_DYNAMIC
和静态的溢出对应,这里主要说的是数组或索引在动态访问内存的时候越界。
由于C/C++语言固有的不安全性,数组或指针的索引不会自动去做边界检查,缓冲区溢出的情况非常普遍,
所以C/C++的程序员有必要自己检查索引是否在有效区间内,但在复杂的函数调用和数据流传递中,这项工作变得异常复杂。
举例说明:
void bad_heap() { int *buffer = (int *) malloc(10 * sizeof(int)); // allocates 40 bytes // (10 * 4 byte ints) int i = 0; // valid indices are buffer[0] to buffer[9] for(; i <= 10; i++) { // overruns memory by writing buffer[10] buffer[i] = i; } }
12 NEGATIVE_RETURNS
函数返回的值可能是负整数。在使用之前(例如数组索引、循环标志变量、代数表达式、函数调用的关于长度大小的参数),应该检查函数返回值的正确性。
负整数的错误使用会导致严重的问题,比如死循环、数组越界(内存冲突)变量溢出等。最常见的错误是将一个无符号的值赋给有符号的整型变量,然后把该变量作为数组的索引。
另外一种情况是将函数返回的负值直接或者隐式的转换成无符号整型变量,这将会导致一个非常大的值,如果该变量正好作为数组的索引或者内存申请的参数,一个进程将会申请大量的内存或者非常大的一个循环等。
举例说明:
void basic_negative() { int buff[1024]; int x = some_function(); // some_function() might return -1 buff[x] = 0; // ERROR: buffer underrun at buff[-1] } void subtle_negative() { unsigned x; x = signed_count_func(); // returns signed -1 on error // -1 cast to an unsigned is a very large integer loop_with_param(x); // uses x as an upper bound // ERROR, loop might never end or last too long } void another_subtle_negative() { unsigned int c; for (i = 0; (c=read(fd, buf, sizeof(buf)))>0; i+=c) // read returns -1 on { // error // c is now a very large integer if (write(1, buf, c) != c) // ERROR: attempts to write too many bytes to stdout { die("Write call failed"); } } }