从一个跨二十年的glibc bug说起
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说起的更多相关文章
- Vue 全家桶 + Electron 开发的一个跨三端的应用
代码地址如下:http://www.demodashi.com/demo/11738.html GitHub Repo:vue-objccn Follow: halfrost · GitHub 利用 ...
- 『开源』仿SQLServer山寨一个 跨数据库客户端
002 Laura.SqlForever项目简单介绍 相关文章 <『练手』001 Laura.SqlForever架构基础(Laura.XtraFramework 的变迁)> <『练 ...
- 一个跨域请求的XSS续
之前讨论过,在解决post跨域请求时,采用iframe+本域代理页的形式,兼容性(当然是包括IE6啦)是最好的.上次提到,代理页面的作用是:执行本域下的回调函数.就是这个原因,给XSS带来了便利.详细 ...
- 转:一个跨WINDOWS LINUX平台的线程类
来源:http://blog.csdn.net/dengxu11/article/details/7232681 继Windows下实现一个CThread封装类之后,这里我再实现一个跨WINDOWS ...
- Spring 循环引用(一)一个循环依赖引发的 BUG
Spring 循环引用(一)一个循环依赖引发的 BUG Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Spring 循环 ...
- android一个下拉放大库bug的解决过程及思考
android一个下拉放大库bug的解决过程及思考 起因 项目中要做一个下拉缩放图片的效果,搜索了下github上面,找到了两个方案. https://github.com/Frank-Zhu/Pul ...
- Microsoft SilverLightt是一个跨浏览器的、跨平台的插件,为网络带来下一代基于.NETFramework的媒体体验和丰富的交互式应用程序。
Microsoft Silverlight是一个跨浏览器的.跨平台的插件,为网络带来下一代基于.NETFramework的媒体体验和丰富的交互式应用程序.Silverlight提供灵活的编程模型,并可 ...
- 移动端H5页面开发,碰到一个字体变大的BUG
移动端H5页面开发,碰到一个字体变大的BUG webkit内核下,对不定高宽的元素可能会放大其字体.那么,就可以设置一个max-width:或者使用-webkit-text-size-adjust: ...
- 一个历时五天的 Bug
一个程序员在没有成长成为架构师之前,几乎都要跟 Bug为伴,程序员有很多时间都是花在了查找各种 Bug上. 我印象深刻的一个Bug, 是一个服务器网络框架无锁队列的 Bug .那个 Bug 连续查找了 ...
随机推荐
- nginx 的安装、优化、服务器集群
一.安装 下载地址:http://nginx.org 找到 stable 稳定版 安装准备:nginx 依赖于pcre(正则)库,如果没有安装pcre先安装 yum install pcre pcr ...
- jquery : 菜单根据url变颜色
//菜单根据url变颜色$(document).ready(function(){ $('#nav li a').each(function(){ if($($(this))[0].href==Str ...
- ECShop 2.x/3.x SQL注入/任意代码执行漏洞
poc地址:https://github.com/vulhub/vulhub/blob/master/ecshop/xianzhi-2017-02-82239600/README.zh-cn.md 生 ...
- 一份热乎的字节跳动客户端面经,已拿Offer
字节面试过程: 4月4号进行内推,7天的简历评估,11号接到电话面试,尽管猝不及防回答仓促,但好在前期准备充分,通过.14号现场面试,次日收到通知,通过,二面.三面都很顺利.20号进行HR面,26号收 ...
- Seaborn基础画图实例
使用seaborn画图时,经常不知道该该用什么函数.忘记函数的参数还有就是画出来的图单调不好看. 所以,本人对seaborn的一些常用的画图函数,并结合实例写成了代码,方便以后查询和记忆. 若代码或注 ...
- vue3.0安装
一 .vue3.0安装 vue3.0安装 个人推荐以下2种 (1). 开发工具的对应代码中 插入CDN <script src="https://unpkg.com/vue@next& ...
- ant的copy标签使用方法
对于ant里拷贝用的标签的用法,此文(来自 http://electiger.blog.51cto.com/112940/39575 )讲得很好,注意其中黑体字部分,今天被这个问题耽误了20分钟. A ...
- arraycopy将数组分为两部分时游标的设置方法
System.arraycopy是复制数组的一个常用工具,它在游标处如何分为两个是一个需要注意的问题,例如下面的示例代码: byte [] src = { 104, 101, 108, 108, 11 ...
- eclipse中添加进新的java项目中文乱码
eclipse中添加进新的java项目中文乱码 添加学习的一些项目进eclipse中,结果其中的中文注释都变成了乱码 右击项目,点最下面的属性,出来新得弹框 在文本文件编码部分可以发现是GBK格式,选 ...
- Java面试常见基础问题
1.equals和==有什么区别? ==比较两个对象在内存里是不是同一个对象,就是说在内存里的存储位置一致. 如:两个String对象存储的值是一样的,但是可能在内存里存储在不同的地方. equals ...