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

2.4.2 C11附录K边界检查接口

第一个内存管理模型(调用者分配,调用者释放)由在<string.h>文件中定义的C字符串处理函数实现,也由OpenBSD的函数strlcpy()和strlcat()和C11附录K边界检查接口实现。调用这些函数之前,可静态或动态分配内存,这种模式使得这个模型的效率最佳。C11附录K提供替代库函数,它们促进了更安全的编程。替代函数验证输出缓冲区对于预期的结果是否足够大,如果不够大,则返回一个故障指示。数据永远不会越过数组末尾写入。所有字符串结果都是以空字符结尾的。

C11附录K边界检查接口的设计目的主要是实现现有函数的更安全的替代品。例如,C11附录K定义了strcpy_s()、strcat_s()、strncpy_s()和strncat_s()函数,分别作为strcpy()、strcat()、strncpy()和strncat()的替代品,适用于源字符串长度未知的或保证小于已知目标缓冲区大小的情况。

C11附录K函数是微软为响应许多众所周知的安全事故而创建的,以帮助改造其现有的遗留代码库。这些函数,随后被提交给ISO/IEC JTC1/SC 22/WG 14国际标准化组织的C编程语言标准化工作组。这些函数已作为ISO/IEC TR 24731-1公布,后来又合并入C11中,以规范性附录中一组可选的扩展形式进行了规定。由于C11附录K函数往往可以用作原来遗留代码中的库函数的简单替换,《C安全编码标准》[Seacord 2008],“STR07-C.使用TR 24731整治现有的字符串操作代码”建议在实现了该附录要求的实现中使用它们用于此目的。(预期这样的实现定义了__STDC_LIB_EXT1__宏。)

附录K还解决了另一个使编写健壮代码复杂化的问题:因为函数返回函数所拥有的静态对象的指针,所以它们是不可重入的。这些函数可能会很麻烦,因为如果该函数被再次调用,也许是被另一个线程调用,以前返回的结果就可能改变。

C11附录K是一个规范性的,但可选的附录,你应该确保它在自己所有的目标平台上可用。尽管这些函数最初由微软开发,但随微软Visual C++2012和早期版本附带的边界检查库的实现并不完全符合附录K,因为在标准化过程中,这些函数发生了变化,而微软的Visual C++并没有相应做出改变。

2.2.1一节的例2.1可以使用C11附录K函数重新实现,如例2.5所示。除了增加了数组边界检查之外,这个程序类似于最初的例子。如果输入8个或更多字符,它的行为是实现定义的行为(通常情况下,该程序将终止)。

例2.5 使用gets_s()从stdin中读取


01  #define __STDC_WANT_LIB_EXT1__ 1
02  #include <stdio.h>
03  #include <stdlib.h>
04
05  void get_y_or_n(void) {
06    char response[8];
07    size_t len = sizeof(response);
08    puts("Continue? [y] n: ");
09    gets_s(response, len);
10    if (response[0] == 'n')
11      exit(0);
12  }

大多数边界检查函数,在检测到错误,如参数无效或输出缓冲区没有足够的可用字节时,会调用一种特殊的运行时约束处理程序(runtime-constraint-handler)函数。此函数可能会打印一个错误信息和中止程序。程序员可以通过set_constraint_handler_s()函数控制哪个处理函数被调用,并可以使处理程序简单地返回,如果需要返回的话。如果处理简单地返回,调用处理程序的函数,用它的返回值提示它的调用者处理失败。安装一个处理程序的程序,退出时必须检查每次调用任何边界检查函数的返回值,并适当地处理错误。《C安全编码标准》[Seacord 2008],“ERR 03-C.当调用TR 24731-1定义的函数时,使用运行约束处理程序”,建议安装运行时约束处理程序,以消除实现定义的行为。

通过一些额外的复杂性成本来消除实现定义的行为,使用C11附录K边界检查函数从标准输入读取的例2.1可以得到改善,如例2.6所示。

例2.6 使用gets_s()从stdin中读取(改进版)


01  #define __STDC_WANT_LIB_EXT1__ 1
02  #include <stdio.h>
03  #include <stdlib.h>
04  
05  void get_y_or_n(void) {
06    char response[8];
07    size_t len = sizeof(response);
08   
09    puts("Continue? [y] n: ");
10    if ((gets_s(response, len) == NULL) || (response[0] == 'n')) {
11       exit(0);
12    }
13  }
14   
15  int main(void) {
16    constraint_handler_t oconstraint =
17      set_constraint_handler_s(ignore_handler_s);
18    get_y_or_n();
19  }

这个例子通过增加一个对set_constraint_handler_s()的调用来安装ignore_handler_s()函数作为运行时约束处理程序。如果把运行时约束处理程序设置为ignore_handler_s()函数,那么任何库函数违反运行时约束时,都将返回到它的调用者。调用者可以在库函数的规格说明的基础上确定是否发生运行约束违反。大多数边界检查函数返回一个非零errno_t。但是,get_s()函数返回一个空指针,以便它可以作为gets()的近似直接替代。

依据《C安全编码标准》[Seacord 2008]“ERR00-C.采用并实施一个一致和全面的错误处理策略”,约束处理程序设置在main(),以便在整个应用程序中保持一致的错误处理策略。自定义的库函数可能希望避免设置特定的约束处理程序的策略,因为它可能与应用程序执行的整体策略产生冲突。在这种情况下,库函数应假定边界检查函数的调用会返回,并检查相应的返回状态。在库函数不设置约束处理程序的情况下,在返回或退出(以防止存在向atexit()注册的函数)之前,函数必须恢复原来的约束处理程序(由set_constraint_handler_s()函数返回)。

C字符串处理函数和C11附录K边界检查函数都需要预先分配存储。一旦目标内存被填满,就不可能增加新的数据。因此,这些函数或者抛弃多余的数据或运行失败。重要的是程序员要确保目标的大小足够容纳要复制的字符数据和空终止字符,如《C安全编码标准》[Seacord 2008]“STR 31-C.保证字符串的存储有足够的空间存放字符数据和空终结符”所述。

C11附录K中定义的边界检查函数并非万无一失。如果把一个无效的大小传递给这些函数之一,它仍然会遭受缓冲区溢出的问题,虽然号称这样的问题都已解决。由于这些函数通常比其传统的与之对应的函数具有更多的参数,所以使用它们之前需要对每个参数的目的有一个深刻的理解。为遗留代码库引入边界检查函数作为与它们对应的传统函数的替代也需要非常谨慎,以免在这个过程中无意中注入了新的缺陷。另外值得一提的是,把每一个C字符串处理函数更换成相应的边界检查函数并不总是恰当的。