请停止使用strncpy

我不断遇到使用strcpy、sprintf、strncpy、_snprintf(仅限Microsoft)、wcsncpy、swprintf和等效的函数的代码。请停下来。还有一些更安全的选择,而且它们实际上需要更少的输入。


这篇文章的重点是固定大小的字符串缓冲区,但是这种技术适用于任何类型的固定长度缓冲区。如果你不使用固定大小的缓冲区,那么这篇文章与你无关。有些人确实使用它们,有时是出于正当的原因,这篇文章是为他们而写的。


我希望strcpy和sprintf的危险不需要解释。这两个函数都不允许指定输出缓冲区的大小,因此缓冲区溢出通常是一个风险。使用strcpy从网络数据包复制数据或将大数组复制到较小的数组中是特别危险的,但是即使您确定字符串适合,也不值得冒这个风险。

“n”函数被认为是危险的

strncpy、unu snprintf和wcsncpy的危险应该是众所周知的,但显然它们不是。这些函数允许您指定缓冲区的大小,但这一点非常重要,它们不能保证空终止。如果您要求这些函数写入的字符数超过将填充缓冲区的字符数,那么它们将停止运行,从而避免缓冲区溢出,但它们不会为null终止缓冲区。为了正确地使用这些函数,你必须做这种无稽之谈。

char buffer[5];
strncpy(buffer, “Thisisalongstring”, sizeof(buffer));
buffer[sizeof(buffer)-1] = 0;

C/C++中的非终止字符串是一个定时炸弹,它正等待销毁代码。我的理解是strncpy是为在字符串中间插入文本而设计的,然后被重新用于“安全”编码,尽管它非常适合。同时,snprintf遵循strncpy模式,但snprintf没有。也就是说,snprintf保证空终止,但strncpy和_snprintf不保证。奇怪的是开发人员会感到困惑吗?奇怪的是,开发人员经常这样做:

// Make snprintf available on Windows:
// Don’t ever do this! These two functions are different!
#define snprintf _snprintf

strlcpy and lstrcpy

strlcpy旨在解决空终止问题-它总是空终止。当然,它比strncpy有改进,但是它在VC++中不是本机可用的。


lstrcpy是一个与之类似的微软设计缺陷,它看起来像strlcpy,但实际上是一个安全缺陷。它使用结构化异常处理来捕获访问冲突,然后返回,因此在某些情况下,它将掩盖崩溃并为您提供一个未终止的缓冲区。令人惊叹。

 

宽字符更糟?


swprintf是一个无法预测的函数。它的名称中缺少'n',但是它接受字符计数,但是它不能保证空终止。足以使人的头爆炸。

其他呢?


如果你发现下面的列表很明显或者很容易记住,那么你可能是个天才,或者是个骗子:


可能会超出缓冲区:strcpy,sprintf

有时null终止:strncpy、_snprintf、swprintf、wcsncpy、lstrcpy

总是空终止:snprintf,strlcpy


这些函数的文档(手册页,MSDN)通常相当薄弱。我想在顶部加粗的字母告诉我它是否会以null结尾,但通常需要非常仔细的阅读才能确定。编写测试程序通常更快。


同样值得强调的是,在上面列出的七个函数中,只有一个函数是可以安全使用的。也不是很好。

做的多,错误多

但是等等,实际上情况更糟。因为事实证明程序员是不完美的人,因此程序员有时会传递错误的缓冲区大小。不经常——可能不会超过百分之一的时间——但这些错误肯定会发生,“小心”并没有实际帮助。我见过开发人员传递硬编码的常量(错误的)、传递命名的常量(错误的)、使用sizeof(错误的缓冲区)或在wchar_t数组上使用sizeof(从而获得字节计数而不是字符计数)。我甚至看到了一段代码,其中传递的是字符串的地址而不是大小,而且由于模板和强制转换的混合,它实际上被编译了!将sizeof()传递给一个需要字符计数的函数是最常见的错误,但它们都会发生,甚至snprintf和strlcpy也被误用。使用注释和/分析可以帮助捕获这些问题,但是我们可以做得更好。

解决方案

我们是程序员,不是吗?如果我们处理字符串的函数很难正确使用,那么我们应该编写新的函数。结果很简单。这里我向您介绍将字符串复制到数组的最安全方法:

    template <size_t charCount>
    void strcpy_safe(char (&output)[charCount], const char* pSrc)
    {
    YourCopyNFunction(output, pSrc, charCount);
    // Copy the string — don’t copy too many bytes.
    //strncpy(output, pSrc, charCount);
    // Ensure null-termination.
    //output[charCount – 1] = 0;
    }

    // Call it like this:
    char buffer[5];
    strcpy_safe(buffer, “Thisisalongstring”);

这种语法有点奇怪,因为它将对整数值(而不是类型)的模板化与通过引用传递数组相结合,这两种方法对于许多程序员来说都是陌生的。有关通过引用传递数组的详细信息,请参阅这篇堆栈溢出文章。或者,您可以非常有效地使用模板魔术,而不必担心它是如何工作的细节。


<note>评论人士正确地指出,strncpy后跟空终止不是strcpy_safe的理想实现,因为它效率低下(strncpy会将所有字节清零到缓冲区末尾),并且可能会将UTF-8字符减半。修复这个问题超出了本文的范围,本篇文章的重点是通过模板魔法自动推断缓冲区大小。所以,别忘了实现你的copynfunction,也许下次我会发布一个版本。</note>


我要求你不要错误地使用这个函数。您可以通过传递一个无效的源指针使其崩溃,但在多年的使用该技术的过程中,我从未见过一个缓冲区大小没有被正确推断的情况。如果传递一个指针作为目标,因为无法推断大小,代码将无法编译。它只使用静态字符串缓冲区作为目标(没有std::string或std::vector),但是可以为这些目标类型生成不同的重载。
我认为strcpy_safe是一个完美的功能。它要么使用正确,要么编译失败。它。是完美的。只有六行。如果你像K&R那样缩进五个。
因为strcpy_safe非常小-它只调用strncpy,然后存储一个0-它将在优化的版本中自动在VC++和gcc中内联。如果您想进一步减小代码大小,可以编写一个非内联helper函数(strlcpy?)这将执行null终止,并让strcpy\u safe调用此函数。这取决于你。
人们当然可以讨论这个名字——也许你更愿意称之为acme_strcpy,或acme_strncpy_safe。我真的不在乎。你甚至可以称之为strcpy,让模板重载神奇地提高代码的安全性。

Unicode码

字符串截断会导致UTF-8编码出现问题。如果你想在一个字符的边界处截断(或者是那个代码点——我不记得了),那么需要添加一些额外的代码来向后扫描到字符边界。这并不复杂,但它超出了本文的讨论范围,这篇文章的重点是使用模板来推断数组大小。

外推法

显然可以为您使用的所有字符串函数生成类似的包装器。你甚至可以发明新的,比如sprintf_cat_safe。事实上,当我编写一个成员函数时,它需要一个指针和一个大小,我通常把它设为私有的,然后编写一个模板包装器来处理这个大小。这是一种多功能的技巧,你应该习惯使用。模板不仅仅用于编写不可读的元代码。

字符串类

是的,说清楚,我知道std::string的存在。不管是好是坏,大多数游戏开发人员都尽量避免动态分配内存,std::string通常就是这样。使用字符数组有合理的理由(更少的分配,更好的缓存位置),即使这些合理的理由仅仅是因为您已经收到了上百万行遗留代码,这些代码在各个方面都存在安全性和可靠性问题。strcpy_safe和friends的独特之处在于,它们允许您通过一个简单的s_strcpy/strcpy_safe来提高代码的安全性和可靠性。

正如我在上面说的,如果你不需要使用固定长度的缓冲区,那么恭喜你,这篇文章不适用于你。

 

posted on 2020-08-11 08:36  活着的虫子  阅读(2082)  评论(0编辑  收藏  举报

导航