scanf系列函数的安全使用

DONG Yuxuan @ Sep 18, 2016 CST

在不牺牲可维护性的前提下安全使用scanf系列函数的实践

前言

scanfsscanf堪称C的杀手级标准库函数,因为使用起来实在是太方便了,但是它们的安全隐患也是众所周知:

char buf[1024];
scanf("%s", buf);

这段代码里scanf没有办法对输入的字符串做长度限制,如果读入的字符串过长,会导致危险的缓冲区溢出。不想放弃scanf系列函数带来的便利,又要消除这种安全隐患,我们需要在格式描述串里加入对长度的限制:

char buf[1024];
scanf("%1023s", buf);

这虽然保证了安全却大大增加了维护的难度,因为出现了magic number,当缓冲区大小改变时我们必须保证对应的格式描述串也得到修改,稍不注意便会滋生bug。

失败的尝试

在通常的C代码中,缓冲区大小通常是使用宏来定义的:

#define MAXL 1023
char buf[MAXL + 1];	

所以我们自然想到在格式描述串中直接使用这个宏定义,可惜这个宏定义是一个整数,没有办法方便地融合进格式描述串,所以我们尝试利用一个转换宏将整数转换为字符串:

#define S(x) "%" #x "s"

#x的作用是将宏参数x变为字符串,例如S(123)会被预处理器替换为"%" "123" "s",根据C99的字面字符串连接语法,这等价于"%123s"。这样我们似乎可以如下使用scanf系列函数:

#define S(x) "%" #x "s"
#define MAXL 1023

char buf[MAXL + 1];	
scanf(S(MAXL), buf);

但这是行不通的,因为C语言规定#后的宏参数不会继续展开,也就是说上面代码的最后一行等价于: scanf("%MAXLs", buf), 这是错误的。

灵机一动:再来一个宏

要解决这个问题,我们引入一个跳板宏:

#define _S(x) "%" #x "s"
#define S(x) _S(x)

考虑S(MAXL),它会被替换为_S(MAXL),由于这里MAXL不在#后,所以会被进一步展开为_S(1023),最后被转换为"%" "1023" "s",这便达到了我们的预期效果,也就是我们可以这样安全地使用scanf系列函数:

#define _S(x) "%" #x "s"
#define S(x) _S(x)

#define MAXL 1023

char buf[MAXL + 1];	
scanf(S(MAXL), buf);