snprintf的截断拼接
今年2月,我收到客户A的反馈,说上个月有几条运维会话没有关联上工单。经过排查,关联工单操作仅有两处。将会话与某条工单关联只需要将该工单的信息赋过去即可,SQL语句大致为:
UPDATE session SET worknote_id = X, worknote_desc = Y WHERE id = Z;
其中X和Y分别代表工单ID和工单描述,Z为待操作会话的会话ID。
逻辑很简单,代码不过5行,但我并没看出问题所在。考虑到程序是多线程的,且会在多个数据库之间切换访问,我在想是否因为这个原因导致偶尔出现的数据库操作失败,然而几周下来并没什么收获。
一个月后,客户B反馈了类似的问题,但这次更离谱:有一条工单居然和之前所有的会话都关联上了!出于问题的严重性,客户要求一周内必须找出原因并予以解决,我只好硬着头皮返回去看那几行代码:
char sql[1024];
snprintf(sql, sizeof(sql), "UPDATE session SET worknote_id = %d, worknote_desc = '%s' \
WHERE id = %d", worknote_id, worknote_desc, sid);
sql_exec(sql, &error_info);
难道是因为worknote_desc中含有单引号?有也无妨,前面已经做过转义处理。
莫非sql缓冲区长度不够?为了排除这种可能,我索要了出问题的会话和工单数据库记录,发现居然有400多个中文字符!
至此,真相逐渐浮出水面:
- 工单描述较短时,拼出的SQL语句是完整的,执行正常,工单与会话关联正常。
- 工单描述太长时,拼出的SQL语句缓冲区存不下,会被snprintf截断,最终的SQL语句不符合语法规范,执行失败,工单与会话关联失败,对应客户A的现象。
- 工单描述长度适中,使得拼出的SQL语句刚好在WHERE前的空白字符处截断,此时的SQL语句是能被执行的,但更新的却是整个表,工单将与所有会话都关联,对应客户B的现象。
教训
- 带n的字符串操作函数为避免越界写做了超长截断处理,但未必就更安全。
- 边界测试很重要。