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

2.5.6 C99

并非所有对strcpy()函数的使用都是有缺陷的。例如,通常可以动态分配所需空间,如例2.13所示。

例2.13 动态分配所需的空间


1  dest = (char *)malloc(strlen(source) + 1);
2  if (dest) {
3    strcpy(dest, source);
4  } else {
5    /* 
处理错误 */
6    ...
7  }

为了保证此代码是安全的,必须对源字符串进行充分验证[Wheeler 2004],例如,要确保该字符串不是过长的。在某些情况下,明显不存在数组越界写的可能。因此,简单地替换所有的strcpy()调用或使得这种调用安全化可能并非特别有效。在其他情况下,用更加安全的替代函数取代strcpy()函数的调用,以消除由编译器或分析工具生成的诊断消息的做法仍是可取的。

C标准的strncpy()函数经常被推荐作为一种替代strcpy()的函数。不幸的是,strncpy()容易产生空字符结尾错误和其他问题,因此不被认为是strcpy()函数的安全的替代函数。

OpenBSD。strlcpy()和strlcat()函数首先出现在OpenBSD 2.4中。这些函数以一种比相应的C标准函数更不容易出错的方式复制和连接字符串。这些函数的原型如下:


size_t strlcpy(char *dst, const char *src, size_t size);
size_t strlcat(char *dst, const char *src, size_t size);

strlcpy()函数从src复制一个以空字符结尾的字符串到dst(至多复制size个字符)。strlcat()函数在dst的末尾附加上以空字符结尾的src字符串(目标缓冲区内的字符数目最多不超过size个)。

为了帮助防止在数组边界外写入,strlcpy()和strlcat()函数接受目标字符串的全部长度作为size参数。

这两个函数都保证对于所有非零长度的缓冲区,目标字符串是以空字符结尾的。

strlcpy()和strlcat()返回它们试图创建的字符串的总长。对strlcpy()而言,这个值就是源字符串的长度;对strlcat()而言,这个值就是目标字符串(拼接前)和源字符串的长度之和。如果要检测截断,程序员需要验证返回值是否小于size参数。如果结果字符串被截断,那么程序员就可以获得存储整个字符串所需的实际字节数,然后重新分配内存并重新进行复制操作。

无论是strlcpy()或strlcat()都不会用空字符填充目标字符串(强制性的空结束符除外)。这就导致其性能与strcpy()接近,与strncpy()相比则好很多。

C11附录K边界检查接口。在C11附录K中,strcpy_s()和strcat_s()函数被定义为与strcpy()和strcat()函数非常接近的替代函数。strcpy_s()函数有一个额外的参数,用于给出目标数组的大小,来防止缓冲区溢出。


1  errno_t strcpy_s( 
2    char * restrict s1, rsize_t s1max, const char * restrict s2 
3  ); 

在没有违反任何约束规则的情况下,strcpy_s()函数与strcpy()非常类似。strcpy_s()函数将源字符串中的字符复制到目标字符数组,直至遇到空结束符为止,并且复制的字符包含结尾的空字符。

strcpy_s()仅在源字符串可被完全复制到目标缓冲区且不引起目标缓冲区溢出的情况下才会调用成功。该函数执行成功时返回0,这意味着所有s2指向的字符串中请求的字符都填充在s1指向的数组内,并且在s1的结果是以空字符结尾的。否则,返回一个非零值。

strcpy_s()函数执行各种运行时约束。如果s1或s2是一个空指针、目标缓冲区的最大长度等于0,或大于RSIZE_MAX,或小于或等于源字符串的长度,或者,如果复制操作发生在重叠对象之间,就会发生运行时约束错误。目标字符串会被设置为空字符串,且函数返回一个非零值,以增加问题的能见度。

例2.14显示了Open Watcom实现的strcpy_s()函数。运行时约束错误检查都在其后加了注释。

例2.14 Open Watcom的strcpy_s()函数实现


01  errno_t strcpy_s( 
02    char * restrict s1, 
03    rsize_t s1max, 
04    const char * restrict s2 
05  ) { 
06    errno_t   rc = -1; 
07    const char  *msg; 
08    rsize_t   s2len = strnlen_s(s2, s1max); 
09    // 
验证运行时约束 
10    if (nullptr_msg(msg, s1) && // s1 
不是 NULL 
11      nullptr_msg(msg, s2) && // s2 
不是 NULL 
12      maxsize_msg(msg, s1max) && // s1max <= RSIZE_MAX 
13      zero_msg(msg, s1max) && // s1max != 0 
14      a_gt_b_msg(msg, s2len, s1max - 1) && 
15                          // s1max > strnlen_s(s2, s1max) 
16      overlap_msg(msg,s1,s1max,s2,s2len) // s1 
与s2 
不重叠 
17    ) { 
18      while (*s1++ = *s2++); 
19      rc = 0; 
20    } else { 
21      // 
运行时约束违反,
将目标字符串置空 
22      if ((s1 != NULL) && (s1max > 0) && lte_rsizmax(s1max)) { 
23      s1[0] = NULLCHAR; 
24      } 
25    // 
现在调用处理程序 
26      __rtct_fail(__func__, msg, NULL); 
27    } 
28    return(rc); 29  }

strcat_s()函数将源字符串中的字符追加到目标字符串的末尾,直至遇到空结束符为止,并且追加的字符包含结尾的空字符。源字符串存储的初始字符将会覆盖目标字符串原来结尾处的空字符。

strcat_s()调用成功时返回0。然而,如果源或目标指针为NULL或者目标缓冲区的最大长度为0或者比RSIZE_MAX更大,则目标字符串将被设为空串并且函数返回一个非零值。strcat_s()函数在目标字符串已满或者没有足够的空间容纳源字符串的情况下将会执行失败。

在没有正确指定目标缓冲区的最大长度的情况下,strcpy_s()和strcat_s()仍然可能会引起缓冲区溢出的问题。

动态分配函数。ISO/IEC TR 24731-2[ISO/IEC TR 24731-2:2010]描述了POSIX的strdup()函数,它也可以用于复制一个字符串。ISO/IEC TR 24731-2没有定义任何替代strcat()的函数。strdup()函数接受一个指向一个字符串的指针,并返回一个指向新分配的复制品字符串的指针。这种内存必须通过把返回的指针传递给free()来收回。

替代函数总结。表2.5总结了本节中描述的字符串复制的一些替代函数。

表2.5 字符串复制函数

表2.6总结了一些本节中描述的strcat()的替代函数。TR 24731-2没有定义strcat()的替代函数。

表2.6 字符串连接函数