sprintf字符串溢出风险与规避:代码示例相关技术疑问解答
我的IoT嵌入式项目采用gcc 4.2.1编译,C语言标准为gnu99、C++为gnu++11,此前通过IDE编译未收到警告,但在onlinegdb中模拟时发现字符串构建存在不安全问题。以下是可运行代码示例:
#include <stdio.h> #include <string.h> int main() { // example 1 works as expected initially - I think it is unsafe char testString1[6]; sprintf(testString1, "123"); sprintf(testString1, "%s456", testString1); printf("%s\n", testString1); // 123456 // example 2 doesn't work as I expected - I think it is intrinsically safe though char testString2[6]; snprintf(testString2, 6, "123"); snprintf(testString2, 6, "%s456", testString2); printf("%s\n", testString2); // 456 // example 3 what I found works and I think is safe (EDIT: I understand now it isn't after reading comments) char testString3[6]; snprintf(testString3, 6, "123"); snprintf(testString3 + strlen(testString3), 6, "456"); printf("%s\n", testString3); // 123456 // example 4 modified example3 for safety char testString4[6]; snprintf(testString4, 6, "123"); snprintf(testString4 + strlen(testString4), sizeof(testString4) - strlen(testString4), "4567"); printf("%s\n", testString4); // 12345 return 0; }
运行输出如下:
main.c: In function ‘main’: main.c:9:29: warning: ‘456’ directive writing 3 bytes into a region of size between 1 and 6 [-Wformat-overflow=] 9 | sprintf(testString1, "%s456", testString1); | ^~~ main.c:9:5: note: ‘sprintf’ output between 4 and 9 bytes into a destination of size 6 9 | sprintf(testString1, "%s456", testString1); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ main.c:15:33: warning: ‘456’ directive output may be truncated writing 3 bytes into a region of size between 1 and 6 [-Wformat-truncation=] 15 | snprintf(testString2, 6, "%s456", testString2); | ^~~ main.c:15:5: note: ‘snprintf’ output between 4 and 9 bytes into a destination of size 6 15 | snprintf(testString2, 6, "%s456", testString2); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 123456 456 123456 12345
技术疑问解答
1. 示例1是否安全?
完全不安全。sprintf没有缓冲区容量限制,当前测试中testString1刚好能放下123456加终止符(共6字节),但只要输入字符串长度稍有变化,就会触发缓冲区溢出——破坏相邻内存数据,引发程序崩溃、数据篡改等问题。gcc给出的-Wformat-overflow=警告已经明确指出了这个风险。
2. 示例2为何未达到预期效果?
snprintf的第二个参数是包含终止符在内的最大写入字节数。第二次调用时,"%s456"展开后需要7字节(原123占3字节+456占3字节+终止符1字节),但testString2总容量只有6字节。snprintf会截断输出以避免溢出,但由于空间不足,最终只能写入456和终止符(共4字节),直接覆盖了原有的123,导致输出不符合预期。
3. 示例3是否真的安全?能否采用该方式构建字符串?
不安全,绝对不能用。第二次调用snprintf时,第二个参数写的是6,但testString3剩余可用空间只有2字节(原字符串123+终止符占4字节,总容量6字节)。snprintf尝试写入456+终止符共4字节,必然超出剩余空间,引发缓冲区溢出——只是当前测试没爆问题,实际嵌入式环境中可能导致严重故障。
补充说明:示例4的安全逻辑
示例4的处理方式是正确的:通过sizeof(testString4) - strlen(testString4)计算出剩余可用空间(包含终止符),将其作为snprintf的第二个参数,确保写入的字节数不会超过缓冲区剩余容量,从根本上避免了溢出问题。运行结果12345符合预期,说明截断逻辑正常生效。
内容的提问来源于stack exchange,提问作者rmarques

