C和C++安全编码(原书第2版)
上QQ阅读APP看书,第一时间看更新

2.4.3 动态分配函数

第二种内存管理模型(由被调用者分配,由调用者释放)是由ISO/IEC TR 24731-2定义的动态分配函数实现。ISO/IEC TR 24731-2定义了许多标准C字符串处理函数的替代品,这些替代品使用动态分配的内存,以确保不会发生缓冲区溢出。因为使用这样的函数需要引入随后的释放缓冲区的额外调用,所以这些函数更适用于新的开发,而不是改造现有代码。

在一般情况下,因为在ISO/IEC TR 24731-2中描述的函数,总是自动调整缓冲区大小以容纳所需的数据,所以这些函数更好地确保了不会发生缓冲区溢出问题。但是,使用动态内存分配的应用程序,可能会遭受拒绝服务攻击,因为其中的数据会一直存在,直到内存耗尽。它们也更容易出现动态内存管理错误,这也可能导致安全漏洞。

例2.1可以使用动态分配函数实现,如例2.7中所示。

例2.7 使用函数getline()从stdin中读入数据


01  #define __STDC_WANT_LIB_EXT2__ 1
02  #include <stdio.h>
03  #include <stdlib.h>
04  
05  void get_y_or_n(void) {
06    char *response = NULL;
07    size_t len;
08  
09    puts("Continue? [y] n: ");
10    if ((getline(&response, &len, stdin) < 0) ||
11        (len && response[0] == 'n')) {
12      free(response);
13      exit(0);
14    }
15    free(response);
16  }

此程序对于任何输入都具有已定义的行为,包括一个假定,即假定一个非常长的、需要耗尽所有可用内存才能容纳的行,应被视为一个“no”回应。因为对getline()函数动态地分配response缓冲区,所以程序必须调用free()来释放已分配的内存。

ISO/IEC TR 24731-2允许在不相应地打开文件的情况下定义流。这种类型的流从内存缓冲区取得输入或把输出写入到内存缓冲区。例如,GNU C库使用这些流来实现sprintf()和sscanf()函数。

与内存缓冲区相关的流和与外部文件关联的文本文件流,具有相同的操作。此外,数据流的方向也是用完全相同的方式确定的。

你可以明确地使用fmemopen()、open_memstream()或open_wmemstream()函数创建一个字符串流。这些函数允许你对字符串或内存缓冲区执行I/O操作。fmemopen()和open_memstream()函数在<stdio.h>中被声明,如下所示。


1  FILE *fmemopen(
2    void * restrict buf, size_t size, const char * restrict mode
3  );
4  FILE *open_memstream(
5    char ** restrict bufp, size_t * restrict sizep
6  );

open_wmemstream()函数是在<wchar.h>中定义的,并具有以下签名:


FILE *open_wmemstream(wchar_t **bufp, size_t *sizep); 

fmemopen()函数打开一个流,使你可以读取或写入指定的缓冲区。open_memstream()函数打开一个面向字节的流来写入一个缓冲区,而open_wmemstream()函数创建一个面向宽字符的流。当用fclose()关闭流或用fflush()刷新流时,bufp和sizep被更新,以包含缓冲区的指针及其大小。只要没有进一步的输出流发生,这些值仍然有效。如果执行额外的输出,必须再次刷新流来存储新的值,才能再次使用它们。一个空字符被写入缓冲区的末尾,但它存储在sizep中的size值中不包括它。

通过调用fmemopen()、open_memstream()或open_wmemstream()创建的一个与内存缓冲区相关联的流的输入和输出操作,发生在内存缓冲区的范围内,受限于实现。对于用open_memstream()或open_wmemstream()打开的流的情况,内存区域动态增长,以适应必要的写操作。对于输出,在刷新或关闭操作期间,数据从函数setvbuf()提供的缓冲区移动到内存流。如果没有足够的内存来增长内存区域,或者操作需要访问相关内存区域以外的地方,相关的操作失败。

例2.8中的程序在第6行打开一个流来写入到内存。

例2.8 打开一个流来写入内存


01  #include <stdio.h>
02  
03  int main(void) {
04    char *buf;
05    size_t size;
06    FILE *stream;
07  
08    stream = open_memstream(&buf, &size);
09    if (stream == NULL) { /* handle error */ };
10    fprintf(stream, "hello");
11    fflush(stream);
12    printf("buf = '%s', size = %zu\n", buf, size);
13    fprintf(stream, ", world");
14    fclose(stream);
15    printf("buf = '%s', size = %zu\n", buf, size);
16    free(buf);
17    return 0;
18  }

在第10行把字符串“hello”写入到流,并且在第11行刷新该流。fflush()的调用更新buf和size,以便第12行的printf()函数输出:


buf = 'hello', size = 5

在第13行把字符串".world"写入流后,在第14行关闭流。关闭流的同时也更新buf和size,以便第15行的printf()函数输出:


buf = 'hello, world', size = 12

size是缓冲区的累计(总数)大小。open_memstream()函数提供了一个更安全的写入内存机制,因为它采用了根据需要动态分配内存的方法。但是,它确实要求调用者来释放分配的内存,如例子的第16行所示。

在安全关键的系统中,往往是不允许动态分配的。例如,MISRA标准要求,“不得使用动态堆内存分配”[MISRA 2005]。一些安全关键系统在初始化过程中可以利用动态内存分配,但在操作过程中不允许。例如,航空电子软件在初始化飞机时可以动态地分配内存,但在飞行过程中不允许。

动态分配函数从广泛应用的现有实现中取得,许多这类函数都包含在POSIX中。