1. 缘起

这几天调gcc 7.5.0 +glibc 2.23的交叉编译工具链,由于gcc 7.5.0的默认打开Werr,偶然发现了glibc一个隐藏了二十年的世纪大bug。

这个bug在glibc 2.0版本刚开始就引入了,但直到2.25版本才最终解决,即使按glibc-2.0.1.bin.alpha-linux.tar.gz 版本的发布时间(04-Feb-1997)到glibc-2.25.tar.bz2 的发布时间(05-Feb-2017),也持续了20年加一天。

用gcc 7.5编译的时候如果使能-Wall -Werror这2个选项(-Wall 英文说明是Enable most warning messages,表示使能大多数告警上报;-Werror表示所有告警都当错误来上报,不可忽略),会报下面的错误:

nss_nisplus/nisplus-alias.c: In function '_nss_nisplus_getaliasbyname_r':
nss_nisplus/nisplus-alias.c:300:12: error: argument 1 null where non-null expected [-Werror=nonnull]
char buf[strlen (name) + 9 + tablename_len];
^~~~~~~~~~~~~
In file included from ../include/string.h:54:0,
from ../sysdeps/generic/hp-timing-common.h:40,
from ../sysdeps/x86_64/hp-timing.h:38,
from ../include/libc-internal.h:7,
from ../sysdeps/x86_64/nptl/tls.h:29,
from ../sysdeps/x86_64/atomic-machine.h:20,
from ../include/atomic.h:50,
from nss_nisplus/nisplus-alias.c:19:
../string/string.h:394:15: note: in a call to function 'strlen' declared here
extern size_t strlen (const char *__s)
^~~~~~
nss_nisplus/nisplus-alias.c:303:39: error: '%s' directive argument is null [-Werror=format-truncation=]
snprintf (buf, sizeof (buf), "[name=%s],%s", name, tablename_val);
^~
cc1: all warnings being treated as errors

如果不使能-Werror,编译器最多会上报告警,程序还是能正常编译通过。上面2个告警分别对strlen的入参和snprintf的字符串格式化参数做了非空检查,根据代码逻辑判断,两处代码如果执行到,调用的入参确实都必然是空指针。

源代码如下:

276 enum nss_status
277 _nss_nisplus_getaliasbyname_r (const char *name, struct aliasent *alias,
278 char *buffer, size_t buflen, int *errnop)
279 {
280 int parse_res;
281
282 if (tablename_val == NULL)
283 {
284 __libc_lock_lock (lock);
285
286 enum nss_status status = _nss_create_tablename (errnop);
287
288 __libc_lock_unlock (lock);
289
290 if (status != NSS_STATUS_SUCCESS)
291 return status;
292 }
293
294 if (name != NULL)
295 {
296 *errnop = EINVAL;
297 return NSS_STATUS_UNAVAIL;
298 }
299
300 char buf[strlen (name) + 9 + tablename_len];
301 int olderr = errno;
302
303 snprintf (buf, sizeof (buf), "[name=%s],%s", name, tablename_val);
304
305 nis_result *result = nis_list (buf, FOLLOW_PATH | FOLLOW_LINKS, NULL, NULL);

可以看出300行对应的strlen函数的入参要求非空,但由于294行做了一个非空的判断并返回,也就是说如果294行的if判断为非,那说明name指针必然为空,这时strlen来获取字符串长度就会异常。

具体会怎么异常?我们可以写个简单的例子:

1 #include <stdio.h>
2 #include <string.h>
3 int main()
4 {
5 printf("%d", strlen(NULL));
6 return 0;
7 }

默认不带任何参数的情况下,gcc会上报告警,但仍然可以编译通过,执行后会出现Segmentation fault:

 1 gcc  test1.c
2 test1.c: In function 'main':
3 test1.c:5:5: warning: null argument where non-null required (argument 1) [-Wnonnull]
4 printf("%d", strlen(NULL));
5 ^
6 test1.c:5:12: warning: format '%d' expects argument of type 'int', but argument 2 has type 'size_t {aka long unsigned int}' [-Wformat=]
7 printf("%d", strlen(NULL));
8 ^
9
10 ./a.out
11 Segmentation fault

编译如果加上-Wall -Werror选项会直接报error编译失败:

1 gcc -Wall -Werror test1.c
2 test1.c: In function 'main':
3 test1.c:5:5: error: null argument where non-null required (argument 1) [-Werror=nonnull]
4 printf("%d", strlen(NULL));
5 ^
6 test1.c:5:12: error: format '%d' expects argument of type 'int', but argument 2 has type 'size_t {aka long unsigned int}' [-Werror=format=]
7 printf("%d", strlen(NULL));
8 ^
9 cc1: all warnings being treated as errors

问题的直接原因还是因为libc库里面的strlen没有做空指针保护,直接访问入参对应的内存了,所以实际上就会出现空指针访问,程序异常退出。

同样的303行的snprintf也要求%s对应的参数不能是空指针,否则也会出现Segmentation fault。

从上面的分析可以看出,有一些warning实际上本身就是错误,应该作为error来处理,在glibc的漫长进化过程中,有很多执行路径可能真的没走到(如果没有100%覆盖率的单元测试,也没有完善的代码review机制,可能永远也没人会发现),或者确实不影响功能的正常发布。但这些告警指向的代码,一旦走到就会出现致命错误。

最终glibc修正代码其实也很简单,就是将294行的“if (name != NULL)”修改成了“if (name != NULL)”,一个运算符用反了。

很多影响非常大的bug,定位之后的实际修改都是简单的一两行代码的事情,但问题的关键是要发现bug并定位bug,并且在bug修正之后的波及测试工作。

这个bug之所有能持续20年没人发现,只能说明glibc中应该还有很多代码在实际场景中没有用到。

2. 编译器的进化

下面这个表格给出了不同clang或者gcc版本新增的代码静态检查的告警计数,为了显得简洁一点,clang7或者更老的clang的所有告警做了一下汇总,gcc 4或者更老的gcc版本的所有告警也做了一下汇总,从中可以看出每次大版本升级,编译器团队都给开发团队提供了一些新的工具能更多的发现自己代码bug的神器。

下面汇总的1204个告警中,有119个告警是clang和gcc都提供的,其他966个告警至少从名称上看是gcc或者clang特有的。其中clang(以clang 12来算)特有的告警检查项有803个,gcc(以gcc 9来算)有178个,单从这个指标看clang在静态检查方面是远胜于gcc的,"2012 ACM Software System Award"大奖实至名归。

不过clang本身是为了支撑llvm的,所以很多与llvm不相关的功能都是直接调用的gcc的库接口,可以认为clang是站在gcc的巨人肩膀上来发布的自己的产品。

当前各个公司都引入了很多静态检查的工具来完善代码质量,但第一步还是要把静态检查工具的老祖宗,也就是编译器,自带的静态检查功能用足用好,再考虑消除其他静态检查工具的问题比较靠谱。走好这一步,引入clang非常必要。

first introduced compiler version

Count of new warning options

clang7 or older 584
clang8 12
clang9 223
clang10 55
clang11 33
clang12 15
gcc 4 or older 172
gcc 5 26
gcc 6 24
gcc 7 35
gcc 8 16
gcc 9 24
Grand Total 1204

从一个跨二十年的glibc bug说起的更多相关文章

  1. Vue 全家桶 + Electron 开发的一个跨三端的应用

    代码地址如下:http://www.demodashi.com/demo/11738.html GitHub Repo:vue-objccn Follow: halfrost · GitHub 利用 ...

  2. 『开源』仿SQLServer山寨一个 跨数据库客户端

    002 Laura.SqlForever项目简单介绍 相关文章 <『练手』001 Laura.SqlForever架构基础(Laura.XtraFramework 的变迁)> <『练 ...

  3. 一个跨域请求的XSS续

    之前讨论过,在解决post跨域请求时,采用iframe+本域代理页的形式,兼容性(当然是包括IE6啦)是最好的.上次提到,代理页面的作用是:执行本域下的回调函数.就是这个原因,给XSS带来了便利.详细 ...

  4. 转:一个跨WINDOWS LINUX平台的线程类

     来源:http://blog.csdn.net/dengxu11/article/details/7232681 继Windows下实现一个CThread封装类之后,这里我再实现一个跨WINDOWS ...

  5. Spring 循环引用(一)一个循环依赖引发的 BUG

    Spring 循环引用(一)一个循环依赖引发的 BUG Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Spring 循环 ...

  6. android一个下拉放大库bug的解决过程及思考

    android一个下拉放大库bug的解决过程及思考 起因 项目中要做一个下拉缩放图片的效果,搜索了下github上面,找到了两个方案. https://github.com/Frank-Zhu/Pul ...

  7. Microsoft SilverLightt是一个跨浏览器的、跨平台的插件,为网络带来下一代基于.NETFramework的媒体体验和丰富的交互式应用程序。

    Microsoft Silverlight是一个跨浏览器的.跨平台的插件,为网络带来下一代基于.NETFramework的媒体体验和丰富的交互式应用程序.Silverlight提供灵活的编程模型,并可 ...

  8. 移动端H5页面开发,碰到一个字体变大的BUG

    移动端H5页面开发,碰到一个字体变大的BUG webkit内核下,对不定高宽的元素可能会放大其字体.那么,就可以设置一个max-width:或者使用-webkit-text-size-adjust: ...

  9. 一个历时五天的 Bug

    一个程序员在没有成长成为架构师之前,几乎都要跟 Bug为伴,程序员有很多时间都是花在了查找各种 Bug上. 我印象深刻的一个Bug, 是一个服务器网络框架无锁队列的 Bug .那个 Bug 连续查找了 ...

随机推荐

  1. vue keep-alive的实现原理和缓存策略

    使用 <!-- 基本 --> <keep-alive> <component :is="view"></component> < ...

  2. Java 中节省 90% 时间的常用的工具类

    前言 你们有木有喜欢看代码的领导啊,我的领导就喜欢看我写的代码,有事没事就喜欢跟我探讨怎么写才最好,哈哈哈...挺好. 今天我们就一起来看看可以节省 90% 的加班时间的第三方开源库吧,第一个介绍的必 ...

  3. ThinkPHP5 5.0.22/5.1.29 远程代码执行漏洞

    http://192.168.49.2:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_ar ...

  4. 一键设置WPS_Office_2019专业版的定时自动备份的批处理文件

    一键设置WPS_Office_2019专业版的定时自动备份的批处理文件 rem ================================================ rem 一键设置WPS ...

  5. 【剑指offer】53 - II. 0~n-1中缺失的数字

    剑指 Offer 53 - II. 0-n-1中缺失的数字 知识点:数组,二分查找: 题目描述 统计一个数字在排序数组中出现的次数. 示例 输入: nums = [5,7,7,8,8,10], tar ...

  6. insert()与substr()函数

    insert()函数与substr()函数 insert()函数: insert ( pos, str2);--将字符串str2插入到原字符串下标为pos的字符前 insert (pos, n, c) ...

  7. 说实话,Android开发月薪3W,谁不酸呢?

    近期有个网友在某匿名区晒字节跳动Offfer,毕业一年月薪3W,引发众多读者羡慕,纷纷留言酸了.酸了.但进大厂的要求还是蛮高的,需要在技术实力上有一定的积累,今天给大家分享一份高质量笔记, 助力大家技 ...

  8. Python语言系列-01-入门

    python的出生与应用 #!/usr/bin/env python3 # author:Alnk(李成果) """ 1,python的出生与应用 python的创始人为 ...

  9. 轻量级状态管理库Pinia试吃

      最近连续看了几个GitHub上的开源项目,里面都用到了 Pinia 这个状态管理库,于是研究了一下,发现确实是个好东西!那么,Pinia 的特点: 轻量化 -- Pinia 体积约1KB,十分轻巧 ...

  10. NOIP 模拟 $17\; \rm 时间机器$

    题解 \(by\;zj\varphi\) 一道贪心的题目 我们先将节点和电阻按左边界排序,相同的按右边界排序 对于每一个节点,我们发现,选取左边界小于等于它的电阻中右边界大于它且最接近的它的一定是最优 ...