无论是 strncpy()
还是更甚的 strncat()
,它们都有着不明显的行为,最好不要使用它们。
strncpy()
如果目标字符串,比如说长度为 255 字节,strncpy()
总会写入这 255 个字节。如果源字符串短于 255 字节,就会用零填充剩余部分。如果源字符串长于 255 字节,则会在复制了 255 字节后停止,导致目标字符串没有空字符终止符。
strncat()
对于大多数“大小已知”的函数(如 strncpy()
、memcpy()
、memmove()
等),大小参数是目标字符串(内存)中的字节数。而对于 strncat()
,大小是目标字符串中已存在的字符串结尾之后的空间大小。因此,只有当您知道目标缓冲区的大小(S
)以及目标字符串当前的长度(L
)时才能安全地使用 strncat()
。此时,strncat()
的安全参数为 S-L
(我们将在其他时间考虑是否存在偏移一的情况)。但是,既然您知道了 L
,就没有必要让 strncat()
跳过前面的 L
个字符;您可以将 target+L
作为开始位置,并直接复制数据。您也可以使用 memmove()
或 memcpy()
,或者甚至使用 strcpy()
、strncpy()
。如果您不知道源字符串的长度,则必须有信心将其截断。
问题代码的分析
char filename[255];
strncpy(filename, getenv("HOME"), 235);
strncat(filename, "/.config/stationlist.xml", 255);
第一行是无可非议的,除非大小被认为太小(或者在未设置$HOME
环境变量的情况下运行程序),但这超出了本问题的范围。调用strncpy()
时没有使用sizeof(filename)
来确定大小,而是使用任意小的数字。虽然不至于出大问题,但不能保证变量的最后20个字节是零字节(甚至其中任意一个都可能不是零字节)。在某些情况下(filename
是全局变量,以前未使用过),可以保证有零字节。
strncat()
调用试图将24个字符附加到filename
字符串的末尾,该字符串可能已经长达232-234个字节,或者可以任意长达235个字节以上。不管怎样,这都是一种明显的缓冲区溢出。使用strncat()
也直接陷入了有关其大小的陷阱中。你说可以在filename
的现有字符串长度之外添加多达255个字符,这是彻头彻尾的错误(除非getenv("HOME")
返回的字符串恰好为空)。
更安全的代码:
char filename[255];
static const char config_file[] = "/.config/stationlist.xml";
const char *home = getenv("HOME");
size_t len = strlen(home);
if (len > sizeof(filename) - sizeof(config_file))
...error file name will be too long...
else
{
memmove(filename, home, len);
memmove(filename+len, config_file, sizeof(config_file));
}
有人会坚称'memcpy()
是安全的,因为字符串不会重叠',在某种程度上他们是正确的,但是重叠应该不是一个问题,而使用memmove()
,这就不是一个问题了。所以我一直使用memmove()
...但我没有进行时间测量来看看它有多大的问题,如果它真的是个问题的话。也许其他人已经做过测量。
总结
- 不要使用
strncat()
。
- 谨慎使用
strncpy()
(注意它在非常大的缓冲区上的行为!)。
- 计划使用
memmove()
或memcpy()
代替;如果您能够安全地进行复制,则知道使其明智的大小。
cppcheck
静态分析器。 - ouah