【问题标题】:Why does strncpy not null terminate?为什么 strncpy 不为空终止?
【发布时间】:2010-11-30 01:06:15
【问题描述】:

strncpy() 据说可以防止缓冲区溢出。但是,如果它在没有空终止的情况下防止溢出,那么后续的字符串操作很可能会溢出。所以为了防止这种情况,我发现自己在做:

strncpy( dest, src, LEN );
dest[LEN - 1] = '\0';

man strncpy 给出:

strncpy() 函数类似,只是复制的 src 不超过 n 字节。因此,如果src 的前n 字节中没有空字节,则结果不会以空值结尾。

没有 null 终止看似无辜的东西,例如:

   printf( "FOO: %s\n", dest );

...可能会崩溃。


除了strncpy(),还有更好、更安全的替代方案吗?

【问题讨论】:

  • 请注意,在 MacOS X (BSD) 上,手册页说('extern char *strncpy(char * restrict s1, const char * restrict s2, size_t n);'):strncpy() 函数最多将 n 个字符从 s2 复制到 s1。如果 s2 的长度小于 n 个字符,则 s1 的其余部分用 `\0' 字符填充。否则,s1 不会终止。
  • 应该是dest[LEN-1] = '\0'; ?
  • 我认为我们会这样复制字符串:int LEN = src.len; str* dest = new char[LEN+1]; strncpy(目标,源,LEN); dest[LEN] = '\0';
  • 如果您确定字符串的大小不会超过目标缓冲区长度,则始终在目标字符串上使用 memset 是最安全的方法。
  • 编写自己的函数,我认为这应该不是一项艰巨的任务

标签: c strncpy


【解决方案1】:

strncpy() 不打算用作更安全的strcpy(),它应该用于将一个字符串插入另一个字符串的中间。

所有那些“安全”的字符串处理函数,例如snprintf()vsnprintf() 都是在后来的标准中添加的修复程序,以缓解缓冲区溢出漏洞等。

Wikipedia 提到 strncat() 可以替代自己编写保险箱 strncpy()

*dst = '\0';
strncat(dst, src, LEN);

编辑

我错过了strncat() 超过 LEN 字符时 null 终止字符串,如果它更长或等于 LEN char's。

无论如何,使用strncat() 而不是任何本地解决方案(例如memcpy(..., strlen(...))/whatever)的意义在于strncat() 的实现可能在库中进行了目标/平台优化。

当然,您需要检查 dst 是否至少包含 nullchar,因此正确使用 strncat() 应该是这样的:

if (LEN) {
    *dst = '\0'; strncat(dst, src, LEN-1);
}

我也承认strncpy()对于将子字符串复制到另一个字符串不是很有用,如果src比n个字符短,目标字符串将被截断。

【讨论】:

  • “它应该用于将一个字符串插入另一个字符串的中间” - 不,它旨在将一个字符串写入一个固定宽度的字段,例如在目录条目中。这就是为什么当(且仅当)源字符串太短时,它会用 NUL 填充输出缓冲区。
  • 设置 *dst='\0' 如何让这更安全?它仍然存在允许您写入超出目标缓冲区末尾的原始问题。
  • 听起来不错,但它不应该是 strncat(dst,src,LEN-1) ,因为它会写一个额外的字符吗?
  • @Jonathan:实际上安全的数据类型是将指向 char 缓冲区的指针与该缓冲区的长度相结合。但我们都知道这不会发生。就个人而言,我厌倦了所有这些努力,使一些本质上不安全的东西(程序员试图准确地尊重缓冲区的长度),更安全一些。并不是说我们目前有 50% 太多的缓冲区溢出,所以如果我们能让字符串处理更安全 50% 就好了:-(
  • +1 表示不要重复 strncpy 在某种程度上是 strcpy 的安全版本的垃圾 - 前者有其自身的一系列问题。
【解决方案2】:

最初,7th Edition UNIX 文件系统(参见 DIR(5))具有将文件名限制为 14 个字节的目录条目;目录中的每个条目由 2 个字节的 inode 编号加上 14 个字节的名称组成,空填充到 14 个字符,但不一定以空结尾。我相信strncpy() 旨在与这些目录结构一起使用 - 或者,至少,它非常适合该结构。

考虑:

  • 14 个字符的文件名不是以空字符结尾的。
  • 如果名称短于 14 个字节,则将其填充为 null 以达到全长(14 个字节)。

这正是通过以下方式实现的:

strncpy(inode->d_name, filename, 14);

因此,strncpy() 非常适合其最初的利基应用。这只是巧合地防止以空字符结尾的字符串溢出。

(请注意,长度为 14 的空填充并不是一个严重的开销 - 如果缓冲区的长度为 4 KB,并且您只想安全地将 20 个字符复制到其中,那么额外的 4075 个空值是严重的过度杀伤,如果您反复向长缓冲区添加材料,则很容易导致二次行为。)

【讨论】:

  • 这种特殊情况可能并不明显,但具有固定长度字符串字段的数据结构并不少见,这些字段是空填充但不是空终止的。事实上,如果要存储固定格式的数据,这通常是最有效的方法。
【解决方案3】:

已经有像 strlcpy 这样可以安全复制的开源实现。

http://en.wikipedia.org/wiki/Strlcpy

在参考文献中有指向来源的链接。

【讨论】:

  • 更不用说,便携、快速、可靠。您仍然可以滥用它,但风险要低几个数量级。 IMO, strncpy 应该被弃用,并用名为 dirnamecpy 或类似的相同函数替换。 strncpy 不是安全的字符串副本,而且从来都不是。
【解决方案4】:

Strncpy 对您的程序的用户的堆栈溢出攻击更安全,但它不能保护您免受程序员犯下的错误,例如打印一个非以 null 结尾的字符串,如您所描述的那样。

您可以通过限制 printf 打印的字符数来避免因您描述的问题而崩溃:

char my_string[10];
//other code here
printf("%.9s",my_string); //limit the number of chars to be printed to 9

【讨论】:

  • 使用精度字段来限制%s 打印的字符数必须是C 语言中最晦涩的特性之一。
  • @DavidThornley 在 sprintf 下的 K&R 中记录得很清楚。
  • @weston:在 Harbison & Steele,这就是我在工作中得到的。现在,除了这两本之外,还有哪些流行的 C 书籍中提到了这一点?每个特性都应该在 K&R 和 H&S 中提及(并在标准中提及),所以如果这是晦涩的标准,则没有晦涩的特性。
  • @DavidThornley 我只是想平衡您的评论,因为通过放置“最晦涩的功能之一”,它会使这个答案看起来很糟糕,人们可能会被推迟使用它。这是错误的,因为它是一个完全有效的、有据可查的功能,与精度字段的任何其他用途一样有据可查。 “晦涩难懂”似乎是一个见仁见智的问题,就我个人而言,我认为它经常被使用。
【解决方案5】:

ISO/IEC TR 24731 中指定了一些新的替代方案(请查看https://buildsecurityin.us-cert.gov/daisy/bsi/articles/knowledge/coding/317-BSI.html 了解信息)。这些函数中的大多数都带有一个附加参数,该参数指定目标变量的最大长度,确保所有字符串都以空字符结尾,并且名称以_s 结尾(代表“安全”?)以区别于之前的“不安全”版本。1

很遗憾,它们仍在获得支持,并且可能不适用于您的特定工具集。如果您使用旧的不安全函数,更高版本的 Visual Studio 将引发警告。

如果您的工具支持新功能,那么为旧功能创建自己的包装器应该相当容易。这是一个例子:

errCode_t strncpy_safe(char *sDst, size_t lenDst,
                       const char *sSrc, size_t count)
{
    // No NULLs allowed.
    if (sDst == NULL  ||  sSrc == NULL)
        return ERR_INVALID_ARGUMENT;

   // Validate buffer space.
   if (count >= lenDst)
        return ERR_BUFFER_OVERFLOW;

   // Copy and always null-terminate
   memcpy(sDst, sSrc, count);
   *(sDst + count) = '\0';

   return OK;
}

您可以更改函数以满足您的需要,例如,始终复制尽可能多的字符串而不会溢出。事实上,如果您将_TRUNCATE 作为count 传递,VC++ 实现可以做到这一点。




1当然,您仍然需要准确了解目标缓冲区的大小:如果您提供 3 个字符的缓冲区但告诉 strcpy_s() 它有 25 个字符的空间,您仍然在麻烦。

【讨论】:

  • 你不能合法地定义一个名字以str*开头的函数,那个“命名空间”在C中是保留的。
  • 但是 ISO C 委员会可以——而且做到了。另见:stackoverflow.com/questions/372980/…
  • @Jonathan:感谢您交叉引用您自己的问题,它提供了许多额外的有用信息。
【解决方案6】:

使用strlcpy(),此处指定:http://www.courtesan.com/todd/papers/strlcpy.html

如果你的 libc 没有实现,那么试试这个:

size_t strlcpy(char* dst, const char* src, size_t bufsize)
{
  size_t srclen =strlen(src);
  size_t result =srclen; /* Result is always the length of the src string */
  if(bufsize>0)
  {
    if(srclen>=bufsize)
       srclen=bufsize-1;
    if(srclen>0)
       memcpy(dst,src,srclen);
    dst[srclen]='\0';
  }
  return result;
}

(由我于 2004 年撰写 - 致力于公共领域。)

【讨论】:

  • 请赐教,为什么你要的结果总是src字符串的长度?在我看来,返回srclen 会更好,因为我们会知道真正复制了多少个字符。
  • @LêQuangDuy,它符合规范 (freebsd.org/cgi/man.cgi?query=strlcpy&sektion=3#end):像 snprintfstrlcat,它返回它的字符串大小尝试写,所以调用者可以提供更大的缓冲区并重新调用函数来存储所有内容。
【解决方案7】:

您可以使用 strncpy() 代替

snprintf(buffer, BUFFER_SIZE, "%s", src);

这是一个单行代码,它最多将 size-1 非空字符从 src 复制到 dest 并添加空终止符:

static inline void cpystr(char *dest, const char *src, size_t size)
{ if(size) while((*dest++ = --size ? *src++ : 0)); }

【讨论】:

  • 我们使用的宏相当于snprintf(buffer, sizeof(buffer), "%s", src)。只要您记得永远不要在 char * 目的地上使用它,就可以正常工作
【解决方案8】:

我一直喜欢:

 memset(dest, 0, LEN);
 strncpy(dest, src, LEN - 1);

事后修复它的方法,但这实际上只是一个偏好问题。

【讨论】:

  • 是否将所有缓冲区初始化为零本身就是一个争论的话题。就个人而言,我更喜欢在开发/调试期间这样做,因为它往往会使错误更加明显,但还有很多其他(“更便宜”)的选择。
  • 您只需将dest[LEN-1] 设置为0 - 如果需要,其他字节将由strncpy() 填充(记住:strncpy(s,d,n) 总是写入n 字节!)
【解决方案9】:

strncpy 直接使用可用的字符串缓冲区,如果您直接使用内存,则必须现在缓冲区大小,并且可以手动设置 '\0'。

我相信在纯 C 语言中没有更好的选择,但如果你在使用原始内存时尽可能小心,它并不是那么糟糕。

【讨论】:

    【解决方案10】:

    这些功能的演变不仅仅是设计出来的,所以真的没有“为什么”。 你只需要学习“如何”。不幸的是,Linux手册页至少是 没有这些函数的常见用例示例,我注意到 很多 我审查过的代码中的误用。我在这里做了一些笔记: http://www.pixelbeat.org/programming/gcc/string_buffers.html

    【讨论】:

    • Erm 为什么上面的 URL 中的 _ 被修改为 %5F?根据 RFC 3548,下划线很好。
    • 鉴于 strncpy() 存在,可以通过在缓冲区末尾手动写入零字节来强制字符串以零结尾。相比之下,如果 strncpy() 坚持总是在最后一个有用的位置之后写入一个零字节,我想不出任何更新零填充(未终止)字符串的有效方法。请注意,已知固定长度的零填充字符串过去和现在仍然是一种在磁盘上存储数据的省时方法;以与磁盘上相同的格式将信息存储在 RAM 中也可以提高性能。
    【解决方案11】:

    在不依赖较新的扩展的情况下,我过去做过这样的事情:

    /* copy N "visible" chars, adding a null in the position just beyond them */
    #define MSTRNCPY( dst, src, len) ( strncpy( (dst), (src), (len)), (dst)[ (len) ] = '\0')
    

    甚至可能:

    /* pull up to size - 1 "visible" characters into a fixed size buffer of known size */
    #define MFBCPY( dst, src) MSTRNCPY( (dst), (src), sizeof( dst) - 1)
    

    为什么使用宏而不是较新的“内置”(?) 函数?因为过去有很多不同的 unice 以及其他非 unix(非 Windows)环境,当我每天做 C 时,我不得不移植到后面。

    【讨论】:

      猜你喜欢
      • 2019-07-09
      • 1970-01-01
      • 2010-10-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多