如何防止未终止的字符串错误

6
在一个C程序中,我有一个函数将消息写入日志文件。
LogResult writeLog(const char* format, ...)

那个函数将它的参数作为'format'字符串和一个'va_list'传递给'vfprintf()'。我突然意识到如果有人传递一个未终止的字符串,比如:

const char unterminatedString[5] = {'h', 'e', 'l', 'l', 'o'};
writeLog("Log message: %s", unterminatedString);

有没有什么方法可以预防这种情况发生?


2
没有什么神奇的:如果你想做到这一点,你必须将每个字符串的长度传递给 "writeLog",并在调用 "vfprintf()" 之前进行适当的检查,或者在 "writeLog" 中构建一个新的格式字符串,限制每个打印字符串的宽度为它们的大小,然后调用 "vfprintf()"。 - Filipe Gonçalves
2
@TonyHopkinson 你怎么知道要插入在哪里? - Eric Fortin
1
插入?你是指追加吗。不过我明白你的意思了。 - Tony Hopkinson
3
由于您标记了C++标签,我建议您使用std::string - Zyx 2000
1
@TonyHopkinson 这个问题的关键是你确切地不知道字符串在哪里结束。 - Filipe Gonçalves
显示剩余4条评论
4个回答

2

除了您提到的方法外,还有许多其他传递非法字符串的方式。

以下是几个例子:

char* unterminatedString;
writeLog("Log message: %s", unterminatedString);

char* unterminatedString = (char*)0xA5A5A5A5;
writeLog("Log message: %s", unterminatedString);

char unterminatedString[] = "abc";
unterminatedString[3] = 'd';
writeLog("Log message: %s", unterminatedString);

int x = -1;
char* unterminatedString = (char*)&x;
writeLog("Log message: %s", unterminatedString);

合法的字符串必须从有效的内存地址开始,并在有效的内存地址结束(以0字符结尾)。除了迭代字符串本身直到达到0字符或执行内存访问违规(这正是vfprintf函数所做的)之外,没有确定的方法来断言这一点。


1
我对关于停机问题的简化方案至少有一个大致的草图感兴趣,因为我根本不相信这个。此外,还有一个事实,即许多语言有很多强制内存安全的手段,尽管这个相关性取决于你的观点。 - user395760
因此,为了完成我的答案 - 如果您有一种方法可以断言给定字符串以有效内存地址的0字符结尾而无需迭代该字符串,则肯定可以应用此魔法并决定机器在给定输入程序上是否停止。 - barak manos
关于停机问题的这件事在理论上很有趣,但是这种情况是否需要一个带有有限磁带的图灵机(由于C内存模型)?那么对于C风格的字符串而言,最坏的情况就是你需要查看磁带上的每个位置并且找不到'\0'。 - Brandin
我想我可能会删除关于停机问题的部分,只留下我之后制作的技术参考。 - barak manos
你应该将其删除,因为我没有看到任何支持它的证据。你的论点表明这两个问题都是不可判定的,但并没有稍微提示与停机问题(或其他不可判定问题)之间的关系。并非所有不可能性都是等效的。例如,具有用于通常停机问题的oracle的TM的停机问题(以下简称TM+oracle)不能由TM+oracle决定,但无法归约到通常的停机问题(否则它将不是不可判定的,因为TM+oracle可以解决通常的停机问题)。 - user395760
显示剩余8条评论

2

const char*并不知道数组的长度,它只是一个地址。所以这并不简单。

如果你想保持纯C,你可以传递字符串的长度int len作为额外的参数,并检查format[len]==0。 但我真的觉得这并没有什么帮助,甚至更糟。

由于你标记了这个问题使用C++,你可以(而且我认为应该)使用std::string

LogResult writeLog(const std::string& format, ...)

std::string始终正确终止,您可以使用std::string::c_str()访问其基础的C风格字符串。

请注意,std::string可以存储\0字符,因此使用C风格字符串的函数会在第一个\0处停止,而不一定是在std::string的结尾处停止。


2

我假设你的日志函数有自己的内部缓冲区,并且你知道它的大小。

然后,您可以使用指定缓冲区大小的 vsprintf() 函数。请参见 可能存在缓冲区溢出漏洞


你说得很对,'writeLog()'确实有一个已知大小的内部缓冲区,我相信'vsnprintf()'就是我问题的答案。不知怎么的,我之前漏掉了这个函数。 - jokki
顺便说一下,我认为如果您传递一个未终止的数组,仍然无法防范 UB。考虑一下,如果传递给 writeLog 函数的未终止数组在缓冲区耗尽之前走神了,那么我认为这仍然是 UB,尽管这种 UB 可能不像溢出缓冲区那样糟糕。 - Brandin
@Brandin,我理解大多数缓冲区溢出是为了防止写入的缓冲区超出缓冲区的末尾。一种可能性是格式字符串没有终止,并且恰好位于具有适当格式说明符的区域中,因此读取垃圾参数会导致日志中的垃圾,但是日志的输出缓冲区不会溢出。 C调用约定将正确返回。请参见https://dev59.com/sGYr5IYBdhLWcg3wG2lI - Richard Chambers

1

不,没有可移植的方法来实现它。

你可以使用类似 valgrind 的工具来查找此类错误。


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