为什么strncpy不为null终止?


76

strncpy()据说可以防止缓冲区溢出。但是,如果它防止了不带null终止的溢出,则很可能随后的字符串操作都会溢出。因此,为了防止这种情况,我发现自己正在做:

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

man strncpy 给出:

strncpy()功能类似,但所复制的n字节数不超过个字节src。因此,如果存在第一间没有空字节n的字节src,则结果将不会被空终止。

在没有null终止的情况下,看起来像是无辜的事情如下:

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

...可能会崩溃。


是否有更好,更安全的替代方法strncpy()


1
请注意,在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'; ?
codeObserver

1
我想这就是我们复制字符串的方式:int LEN = src.len; str * dest = new char [LEN + 1]; strncpy(dest,src,LEN); dest [LEN] ='\ 0';
codeObserver

如果您确定字符串的大小不会超过目标缓冲区的长度,则始终在目标字符串上使用memset是最安全的方法。
koolvcvc 2014年

写你自己的函数,我不认为这应该是一个艰巨的任务
Megharaj

Answers:


45

strncpy()并不是为了更安全而使用strcpy(),而是应该在另一个字符串中间插入一个字符串。

所有这些“安全”字符串处理功能(例如snprintf()vsnprintf()是在更高标准中添加的修补程序,以减轻缓冲区溢出漏洞等)。

维基百科提到strncat()了写自己的保险箱的替代方法strncpy()

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

编辑

我错过了strncat()在长度大于或等于LEN char的字符串终止为null时超过LEN个字符的情况。

无论如何,使用strncat()替代任何本地解决方案(例如memcpy(..., strlen(...))/ whatever)的目的是,strncat()库中的目标/平台可能会优化实现。

当然,您需要检查dst是否至少包含nullchar,因此正确的使用方法strncat()如下:

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

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


28
“应该用来在另一个字符串中间插入一个字符串”-不,它旨在将字符串写入固定宽度的字段,例如在目录条目中。这就是为什么(仅在)源字符串太短时用NUL填充输出缓冲区的原因。
史蒂夫·杰索普

3
设置* dst ='\ 0'如何使其更安全?它仍然具有允许您在目标缓冲区末尾之外写入的原始问题。
亚当·利斯

3
听起来不错,但考虑到要写一个额外的字符,它不应该是strncat(dst,src,LEN-1)吗?
蒂莫西·普拉特利

3
@Jonathan:实际上安全的是将一个指向char缓冲区的指针与该缓冲区的长度组合在一起的数据类型。但是我们都知道这不会发生。就我个人而言,我为所有这些使本来就不安全的事情(程序员试图准确地遵守缓冲区的长度)所做的所有努力都感到厌倦,而其中的某些部分则更加安全。这并不是说我们当前有50%的缓冲区溢出,所以如果我们只能使字符串处理安全50%,那就没问题了:-(
Steve Jessop

1
+1表示不重复说strncpy是strcpy的安全版本-前者有其自身的问题。
paxdiablo

25

最初,第7版UNIX文件系统(请参阅DIR(5))具有目录条目,该目录条目将文件名限制为14个字节。目录中的每个条目都由2个字节的inode编号和14个字节的名称组成,将null填充为14个字符,但不一定以null结尾。我认为,这种strncpy()设计旨在与这些目录结构一起使用-或至少可以完美地适用于该结构。

考虑:

  • 14个字符的文件名不能为空终止。
  • 如果名称少于14个字节,则将其空填充为全长(14个字节)。

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

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

因此,strncpy()非常适合其原始的细分市场应用。巧合的是,防止以null终止的字符串溢出。

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


2
这种特殊情况可能是晦涩难懂的,但是具有固定长度的字符串字段的数据结构并没有填充空值,但是没有以空值结尾的情况并不少见。确实,如果要存储固定格式的数据,这通常是最有效的方式。
supercat

24

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

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

在参考文献中有到资源的链接。


1
更不用说,便携,快速,可靠。您仍然可以滥用它,但是风险要低几个数量级。IMO,应弃用strncpy,并用相同的函数dirnamecpy或类似的函数代替。strncpy不是安全的字符串副本,并且从来没有过。

9

strncpy()函数是通过对栈溢出攻击的安全用户程序的,它并不能保护你避免发生差错,你的程序员做的,比如打印一个非空结尾的字符串,你所描述的方式。

您可以通过限制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 2012年

@weston:在Harbison&Steele,这就是我在这里工作的内容。现在,除了这两本书以外,在哪些流行的C书中都提到了这一点?每个功能都应在K&R和H&S中提及(并在标准中提及),因此,如果这是晦涩难懂的标准,那么就不会有晦涩难懂的功能。
David Thornley,2012年

@DavidThornley我只是想平衡您的评论,因为通过放置“最模糊的功能之一”,它会使此答案看起来很糟糕,并且人们可能会推迟使用它。这是错误的,因为它是完全有效的,有据可查的功能,以及与Precision字段的任何其他用法一样有据可查。“晦涩”似乎是一个意见问题,因为我个人认为这很有用。
weston 2012年

8

ISO / IEC TR 24731中指定了一些新的替代方法(有关信息,请参见https://buildsecurityin.us-cert.gov/daisy/bsi/articles/knowledge/coding/317-BSI.html)。这些函数中的大多数都带有一个附加参数,该参数指定目标变量的最大长度,确保所有字符串都以null终止,并具有以_s(以“ safe”?结尾)的名称结尾,以将它们与较早的“ unsafe”版本区分开来。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;
}

您可以更改功能以满足自己的需要,例如,始终复制尽可能多的字符串而不会溢出。事实上,如果你通过VC ++实现可以做到这一点_TRUNCATEcount




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


您不能合法定义名称以str *开头的函数,该“命名空间”保留在C
解开

2
但是ISO C委员会可以-并且做到了。另请参阅:stackoverflow.com/questions/372980/…–
乔纳森·莱夫勒

@Jonathan:感谢您对自己的问题的交叉引用,该问题提供了许多其他有用的信息。
亚当·利斯

5

使用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êQuang Duy)'18年

@LêQuangDuy,它符合规范(freebsd.org/cgi/man.cgi?query=strlcpy&sektion=3#end):像snprintfstrlcat一样,它返回它尝试写入的字符串的大小,因此调用者可以提供更大的缓冲区并重新调用该函数来存储所有内容。
乔纳森·利德贝克

3

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

我相信在普通C语言中没有更好的替代方法,但是如果您在处理原始内存时要格外小心,它的确不是那么糟糕。


3

代替strncpy(),您可以使用

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

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

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 *目的地上使用过它,它就可以正常工作
che

3

我一直喜欢:

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

事后解决问题,但这实际上只是一个优先事项。


1
是否将所有缓冲区初始化为零是一个有争议的话题。就个人而言,我更喜欢在开发/调试期间这样做,因为它会使错误更加明显,但是还有很多其他(“便宜”)的选择。
亚当·利斯

7
您只需设置dest[LEN-1]0-其他字节将在需要时填充strncpy()(请记住:strncpy(s,d,n)始终写入n字节!)
Christoph

2

这些功能的发展远不止是设计,因此确实没有“为什么”。您只需要学习“如何”。不幸的是,Linux手册页至少没有这些功能的常见用例示例,而且我注意到 在我所审查的代码中有很多误用。我在这里做了一些说明:http : //www.pixelbeat.org/programming/gcc/string_buffers.html


呃,为什么_在上面的URL中被拼成%5F?下划线是罚款按照RFC 3548
pixelbeat

如果strncpy()存在,则可以通过在缓冲区末尾手动写入零字节来强制字符串以零结尾。相比之下,如果strncpy()坚持总是在最后一个有用位置之后写入一个零字节,那么我想不出任何有效的方式来更新填充零(未终止)的字符串。请注意,已知固定长度的零填充字符串现在仍然是一种省时的方法,可以将数据存储在磁盘上。以与磁盘上相同的格式将信息存储在RAM中也可以提高性能。
supercat

2

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

/* 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,以及其他每天在执行C语言时必须移植回的非unix(非Windows)环境。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.