你是否使用TR 24731中的“安全”函数?

89

ISO C委员会(ISO / IEC JTC1 / SC21 / WG14)已经发布了TR 24731-1并正在制定TR 24731-2

TR 24731-1:C库扩展第I部分:边界检查接口

WG14正在制定关于更安全的C库函数的TR。该TR旨在通过添加具有缓冲区长度的额外参数来修改现有程序。最新草案在文档N1225中。原因在文档N1173中。这将成为技术报告类型2。

TR 24731-2:C库扩展第II部分:动态分配函数

WG14正在制定关于更安全的C库函数的TR。该TR旨在针对使用动态分配而不是具有缓冲区长度的额外参数的新程序。最新草案在文档N1337中。这将成为技术报告类型2。

问题

  • 你是否使用支持TR24731-1函数的库或编译器?
  • 如果是,使用的是哪个编译器或库以及在哪些平台上?
  • 在修复代码以使用这些函数时,是否发现了任何错误?
  • 哪些函数提供最大价值?
  • 有哪些函数没有提供价值或者提供了负面价值?
  • 您是否计划在未来使用该库?
  • 您是否正在跟踪TR24731-2的工作?

1
@MarcusJ:嗯——我需要澄清一下你所说的“将strlen()添加到代码中”的含义。有时候strlen()显然不是正确的答案,比如当将缓冲区传递给I/O函数(例如gets_s())时。但也许你可以详细说明一下你的想法? - Jonathan Leffler
1
@MarcusJ:你不能使用realloc(),因为需要保护的函数不会分配内存。例如,strcpy()函数不会进行内存分配;即使有垃圾回收,你也不能合理地修改它以执行内存分配,因为人们通常不使用返回值,而是使用作为第一个参数传递给strcpy()的值进行进一步操作。gets()strcat()也存在类似的问题。至少它们返回一个可能指向重新分配空间的char *(并不保证参数已经分配)。_ […继续…]_ - Jonathan Leffler
1
问题在于像sprintf()这样不返回char *的函数更糟糕;它们没有办法告诉调用代码它们“重新分配”了结果所在的内存。请注意,TR 24731-2未能成为C11的原因之一是它们将是显示进行内存分配的第一个函数,除了malloc()等函数外。请花时间研究这些函数的功能、Annex K / TR 24731-1 函数的功能、它们这样做的原因等等。做出这些决策有一些合理的理由。 - Jonathan Leffler
1
嗯,这个问题现在不太适合在Stack Overflow上发布 ;) - Antti Haapala -- Слава Україні
1
@AnttiHaapala:可能不是(尽管我认为SO现在变得有点太严格了)。 我想为其争取至少历史地位(历史锁定)。 它可以重新表述为“TR24731(Annex K)函数可用吗?”,但是…… 特别是,我认为我的答案中的信息对C程序员很有用,并且应该托管在SO的C部分中。 曾经,它可能已被纳入“文档”中-现在不会发生这种情况。 - Jonathan Leffler
显示剩余6条评论
5个回答

71
自从这些TR(Technical Reports)开始出现(当时只是一个单一的TR),我就一直是它们的批评者,我不会在我的任何软件中使用它们,因为它们掩盖了症状而不是解决问题。我认为,如果有什么作用的话,它们会对软件设计产生负面影响,因为它们提供了虚假的安全感,而不是促进能够更有效地实现相同目标的现有做法。事实上,除了开发这些TR的委员会之外,我不知道还有哪个主要支持者。
我使用glibc,因此知道我将被免于处理这种无聊的事情,如glibc的首席维护人员Ulrich Drepper 在该主题上所说的那样:
提议的ISO C库无法完全解决问题。 ...建议让程序员的生活更加困难并没有意义。 但这正是所提出的内容... 它们都需要更多的工作或者只是愚蠢。
他接着详细说明了一些拟议功能的问题,并在别处指出glibc永远不会支持这一点。
Austin Group(负责维护POSIX)对TR提出了非常批评性的评论,他们的评论和委员会的回复在此处可用。Austin Group的评论非常详细地描述了TR的许多问题,因此我不会在此介绍每个细节。

总之,我的观点是:我不使用支持或将支持此功能的实现,也没有计划在将来使用这些函数,而且我认为 TR 没有任何积极价值。我个人认为,TR 之所以仍然以任何形式存在,仅仅是因为它受到了微软的强烈推动,尽管存在广泛反对。如果这些函数被标准化,我认为它们永远不会得到广泛应用,因为该提案已经存在几年了,却未能赢得真正的社区支持。


24
引用Ulrich Drepper的观点作为任何权威的证据都是一个很好的方式,可以立即击败你的论点,而不考虑其他有益的情况。 - Pavel Minaev
64
@Pavel,我引用Drepper作为glibc的权威。无论你对他有什么个人问题,他都是glibc的首席维护者,几乎决定了哪些内容将被包含在glibc中,不管你喜欢与否。我并没有利用他的意见来反对TR,你的评论似乎基于对某个人的强烈个人敌意,如果这使你无法看到更大的图景,那就是你需要解决的问题。 - Robert Gamble
8
“不知道自己在做什么的人应该使用VB,而不是C :-)” - paxdiablo
3
我在一家大型公司工作(员工超过6万人,主要从事工程领域),现在所有新代码的标准要求都需要使用这个库。我同意它会给不了解情况的人提供虚假的安全感,但是稍微增加些许安全性总比没有好。 - Phil Hord
9
为了获胜:多个libupnp缓冲区溢出。你不喜欢的那些更安全的函数将会阻止其中大部分。在宣传错误建议方面,做得好 :) - jww
13
如果使用懂得正确编程的程序员,所有这些问题都可以避免... - John Hascall

36

问题的直接回答

我喜欢Robert的回答,不过我对自己提出的问题也有些看法。

  • 您是否使用支持TR24731-1函数的库或编译器?

    不,我没有。

  • 如果是这样,使用哪个编译器或库以及在哪些平台上?

    我认为这些函数由MS Visual Studio(例如MS VC++ 2008版)提供,并且有警告鼓励您使用它们。

  • 您是否因修复代码而使用这些函数而发现任何错误?

    还没有。我不指望在我的代码中发现很多错误。我可能会在处理的其他代码中发现一些错误。但我还没有被说服。

  • 哪些函数提供最大价值?

    我喜欢printf_s()系列函数不接受'%n'格式说明符的特点。

  • 有哪些函数提供的价值为零或负值?

    tmpfile_s()和tmpnam_s()函数令人非常失望。它们真的需要更像mkstemp(),即创建文件并打开文件以确保没有TOCTOU(检查时间与使用时间不一致)漏洞。就其现状而言,这两个函数提供非常少的价值。

    我还认为strerrorlen_s()提供的价值很小。

  • 您是否计划在未来使用该库?

    我对此有两种想法。我开始编写一个库,以便实现TR 24731的功能,同时基于标准C库进行开发,但是被需要展示它正常工作的大量单元测试所困扰。我不确定是否要继续下去。我有一些代码希望在Windows上进行移植(主要是出于通用支持所有平台的倔强想法——这些代码已经在Unix派生产品上运行了几十年)。不幸的是,要使其编译时不受MSVC编译器的警告影响,我必须在代码中添加一些东西,以防止MSVC对我使用完全可靠(当小心使用时)的标准C库函数抱怨。这让我感到非常不爽。已经足够糟糕,我必须处理超过两个十年的系统发展;不得不应对别人的有趣想法(让人们无需使用TR 24731就要求他们采用)更令人恼火。这部分是我开始开发库的原因——允许我在Unix和Windows上使用相同的接口。但是我不确定我将从哪里继续下去。

  • 您是否在追踪TR24731-2的进展?

    在收集问题数据时,我并没有追踪它,直到我访问了标准网站。 asprintf()vasprintf()函数可能很有价值;我会使用它们。对于内存流I/O函数,我不确定。C级别将strdup()标准化,这将是一个巨大的进步。对我来说,这似乎比第一部分(边界检查)接口更少争议。

    总的来说,我不太相信第一部分“边界检查接口”。第二部分“动态分配函数”的草案材料更好。如果由我决定,我会沿着第一部分的方向前进,但也要修订在C99标准C库中返回字符串开头的char *的接口(例如strcpy()strcat()),以便它们返回指向新字符串结尾的空字节的指针。这将使一些常见的惯用语(例如重复将字符串连接到另一个字符串末尾)更加高效,因为它将使避免重复使用strcat()的代码所展示的二次行为变得微不足道。所有替换都将确保输出字符串以null结尾,就像TR24731版本那样。我不完全反对检查接口的想法,也不反对异常处理函数。这是一个棘手的问题。

    更新(2011-05-08)

    另请参阅此问题。遗憾的是,对于TR24731函数的有用性来说,一些函数的定义在Microsoft实现和标准之间存在差异,使它们对我来说毫无用处。我的回答引用了vsnprintf_s()

    例如,TR 24731-1表示vsnprintf_s()的接口如下:

    #define __STDC_WANT_LIB_EXT1__ 1
    #include <stdarg.h>
    #include <stdio.h>
    int vsnprintf_s(char * restrict s, rsize_t n,
                    const char * restrict format, va_list arg);
    

    不幸的是,MSDN 上对 vsnprintf_s() 接口的说明如下:

    int vsnprintf_s(
       char *buffer,
       size_t sizeOfBuffer,
       size_t count,
       const char *format,
       va_list argptr 
    );
    

    参数

    • buffer - 输出缓冲区的存储位置。
    • sizeOfBuffer - 输出缓冲区的大小。
    • count - 写入字符的最大数目(不包括终止空字符)或 _TRUNCATE。
    • format - 格式化说明符。
    • argptr - 指向参数列表的指针。

    需要注意的是,这不仅仅是类型映射的问题:固定参数的数量是不同的,因此无法调和。另外,对于具有 'sizeOfBuffer' 和 'count' 的功能,我也不清楚(也许标准委员会也不清楚)有何好处;看起来像是相同的信息重复了两遍(或者至少通常情况下,两个参数将使用相同的值编写代码)。

    scanf_s() 及其相关函数也存在问题。 Microsoft 指出缓冲区长度参数的类型为 unsigned(明确表示“大小参数的类型为 unsigned,而不是 size_t”)。相比之下,在附录 K 中,大小参数的类型为 rsize_t,这是 size_t 的限制变体(rsize_tsize_t 的另一个名称,但 RSIZE_MAXSIZE_MAX 小)。因此,调用 scanf_s() 的代码必须在 Microsoft C 和标准 C 中编写不同的代码。

    最初,我计划使用“安全”函数作为一种在 Windows 和 Unix 上使代码能够编译而无需编写条件代码的方式。由于 Microsoft 和 ISO 函数并不总是相同,因此现在放弃这种计划已经是时候了。


    Visual Studio 2015 中 Microsoft vsnprintf() 的更改

    在 Visual Studio 2015 的 vsnprintf() 文档中,注意到接口发生了变化:

    从 UCRT 开始(Visual Studio 2015 和 Windows 10),vsnprintf 不再与 _vsnprintf 相同。 vsnprintf 函数符合 C99 标准;_vnsprintf 保留了向后兼容性。

    然而,Microsoft vsnprintf_s() 的接口未更改。


    Microsoft 和 Annex K 之间的其他差异示例

    localtime_s() 的 C11 标准变体在 ISO/IEC 9899:2011 附录 K.3.8.2.4 中定义为:

    struct tm *localtime_s(const time_t * restrict timer,
                           struct tm * restrict result);
    

    相对于MSDN版本的localtime_s(),定义如下:

    errno_t localtime_s(struct tm* _tm, const time_t *time);
    

    同时还有 POSIX 变种localtime_r(),其定义如下:

    struct tm *localtime_r(const time_t *restrict timer,
                           struct tm *restrict result);
    

    C11标准和POSIX函数除了名称之外是等效的。尽管与C11标准共享名称,但Microsoft函数在接口上有所不同。

    另一个差异的例子是Microsoftstrtok_s()和Annex K的strtok_s()

    char *strtok_s(char *strToken, const char *strDelimit, char **context); 
    

    vs:

    char *strtok_s(char * restrict s1, rsize_t * restrict s1max, const char * restrict s2, char ** restrict ptr);
    
    请注意,Microsoft版本有3个参数,而Annex K版本有4个参数。这意味着Microsoft的strtok_s()的参数列表与POSIX的strtok_r()兼容 - 因此,如果更改函数名称(例如通过宏),则对这些调用可以实现互换 - 但标准C(Annex K)版本与两者都不同,并多了一个额外的参数。
    在Mac和Linux上声明qsort_r()不同的问题有一个答案,还讨论了由Microsoft定义的qsort_s()和TR24731-1定义的qsort_s()——接口也是不同的。

    ISO/IEC 9899:2011 - C11标准

    C11标准(2010年12月草案;曾经可以从ANSI网上商店以30美元购买ISO/IEC 9899:2011的最终标准PDF副本)将TR24731-1函数作为可选部分包含在其内。它们定义在Annex K(边界检查接口)中,该附件是规范性的而不是信息性的,但是它是可选的。
    C11标准没有TR24731-2功能 - 这很遗憾,因为vasprintf()函数及其相关函数可能非常有用。
    快速摘要:
    • C11包含TR24731-1
    • C11不包括TR24731-2
    • C18与C11在TR24731方面相同。

    建议从C11的后继版本中删除Annex K

    Deduplicator另一个问题的评论中指出,ISO C标准委员会(ISO/IEC JTC1/SC22/WG14)正在考虑删除Annex K。

    文档中提到了一些Annex K函数的现有实现,但它们并不广泛使用(如果您感兴趣,可以通过文档找到它们)。

    文档最后建议:

    因此,我们建议将Annex K从下一版C标准中删除,或者弃用然后删除。

    我支持这个建议。

    C18标准没有改变Annex K的状态。有一篇论文N2336主张对Annex K进行一些修改,修复其缺陷而不是完全删除它。


3
如果微软违反标准,那么需要变更的是微软而不是标准本身。 - cmaster - reinstate monica
5
我也希望如此,但由于他们有一定的用户基础并且不会破坏向后兼容性,在实践中这意味着微软仍然不支持比C89(C90)更现代的C标准,这真是令人遗憾。 - Jonathan Leffler
1
你可以尝试在Windows上使用clang编译器https://clang.llvm.org/get_started.html。它支持C17,并且与Visual Studio工具相当无缝地配合使用。 - annoying_squid

9

好的,现在介绍一下TR24731-2:

是的,我自从在glibc中发现后就一直使用asprintf()/vasprintf(),而且我非常支持它们。

为什么呢?
因为它们一次又一次地能精确地提供我所需要的东西:一种强大、灵活、安全并且(相对)易于使用的格式化任何文本为新分配字符串的方法。

我也非常支持memstream函数:open_memstream()(而不是fmemopen()!!!)像asprintf()一样,会为您分配一个足够大的缓冲区,并给您一个FILE*来执行打印操作,这样您的打印函数可以完全不知道它们是在打印到字符串还是文件中,而且您可以简单地忘记需要多少空间。


感谢您的反馈。遗憾的是,TR24731-2不是C2011标准的一部分,但通常是一组有用的函数。我对POSIX中的fmemopen()函数也有保留意见。open_memstream()函数很有趣。我怀疑在使用它时会有一些陷阱,因为您需要传递指向缓冲区指针和大小变量的指针。但总的来说,TR23731-2还是不错的。 - Jonathan Leffler
我更希望看到的不是vasprintf,而是一个“通用”的vformat函数,它接受一个int(*func)(void*,size_t,char const*)和一个void*,除了通常的vprintf参数之外,并为要输出的每个“span”调用提供的函数[如果函数返回非零值,则提前返回]。可以通过这样的函数合成自动分配sprintf,但通用版本也与自定义分配器兼容。 - supercat
@supercat 在这些方面,已经有一些 stdio 变体可以让您指定自己的读/写函数。例如,您可以执行 FILE *fp = ffunopen(myreadfunc, mywritefunc),然后调用 fprintf任何 stdio 函数,并调用您的回调函数。我还看到过至少一个实现了具有内置自动分配的 fmemopen 变体 - 这意味着您可以为任何输出调用序列获得自动分配,而不仅仅是 *printf - Steve Summit
@SteveSummit:让FILE*包含一个指向函数表的指针将非常有用,但我的主要观点是库例程不应依赖于malloc,而是允许用户代码使用最适合预期使用情况的任何方式来管理内存。 - supercat

6
您是否使用支持TR24731-1功能的库或编译器?如果是,您使用的是哪个编译器或库以及在哪些平台上?
是的,Visual Studio 2005和2008(显然是用于Win32开发)。
由于修复代码使用这些功能而发现了任何错误吗?
有点...... 我编写了自己的安全函数库(仅约15个我们经常使用的函数),将在多个平台上使用--Linux、Windows、VxWorks、INtime、RTX和uItron。创建安全函数的原因是:
- 我们遇到了大量由于不正确使用标准C函数而导致的错误。 - 我对传递给TR函数、它们的POSIX替代品中返回的信息或在某些情况下没有满意。
一旦函数被编写,就会发现更多的错误。 所以,使用这些函数确实是有价值的。
哪个函数提供的价值最大?
vsnprintf、strncpy、strncat的更安全版本。
是否存在任何无价值或负价值的函数?
fopen_s和类似函数对我个人来说增加的价值非常小。 如果fopen返回NULL,我没问题。 您应始终检查函数的返回值。 如果某人忽略了fopen的返回值,那么他们会检查fopen_s的返回值是什么呢? 我理解fopen_s将返回更具体的错误信息,这在某些情况下可能很有用。 但是对于我正在处理的东西,这并不重要。
您是否计划将来使用库?
我们现在正在使用它--在我们自己的“安全”库中。
您是否跟踪TR24731-2的工作?
没有。

5
不,这些函数绝对没有任何用处,除了鼓励编写只能在Windows上编译的代码之外。
snprintf是完全安全的(实现正确时),因此snprintf_s是无意义的。如果缓冲区溢出(通过清除连接到的字符串),strcat_s将销毁数据。还有许多其他完全不懂事物工作方式的例子。
真正有用的功能是BSD strlcpy和strlcat。但是,微软和Drepper出于自私的原因拒绝了这些功能,这让C程序员感到恼火。

2
感谢您的输入。我不确定“完全无知”是否合适,但我同意新功能并不总是像它本应该有的那样改进。 - Jonathan Leffler
3
我认为像strlcat这样的函数更有效的方式是接受一个指向目标缓冲区结尾的指针,并返回指向目标末尾空字节的指针。这将允许从一次调用中返回的返回值被传递给另一个调用,以便将多个值连接到字符串上,而无需每次重新扫描目标字符串。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接