未定义行为之 NULL dereference

下面这段代码中 is_valid() 解引用了空指针 str,我们的直觉是编译运行后将迎来 SIGSEGV,然而事情并非所期望的那样。

/*
* ub_null.c - 未定义行为演示 之 NULL dereference
*/
#include <stdio.h>
#include <string.h> int is_valid(const char *str)
{
if(*str == 0x80) return 1;
if(str == NULL) return 0;
return strcmp(str, "expected string") == 0;
} int main(void)
{
const char *str = NULL;
printf("%d\n", is_valid(str));
return 0;
}
lyazj@HelloWorld:~$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O0
ub_null.c: In function ‘is_valid’:
ub_null.c:6:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
6 | if(*str == 0x80) return 0;
| ^~
lyazj@HelloWorld:~$ ./ub_null
0

结合 GCC 发出的警告,不难推断出条件表达式 *str == 0x80 在编译期被求值且相应的 if 语句被优化掉了,而且这是在 O0 的优化等级下。以下的反汇编结果验证了这一点。

lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null

ub_null:     file format elf64-x86-64

Disassembly of section .text:

0000000000001169 <is_valid>:
1169: f3 0f 1e fa endbr64
116d: 55 push %rbp
116e: 48 89 e5 mov %rsp,%rbp
1171: 48 83 ec 10 sub $0x10,%rsp
1175: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1179: 48 83 7d f8 00 cmpq $0x0,-0x8(%rbp)
117e: 75 07 jne 1187 <is_valid+0x1e>
1180: b8 00 00 00 00 mov $0x0,%eax
1185: eb 1e jmp 11a5 <is_valid+0x3c>
1187: 48 8b 45 f8 mov -0x8(%rbp),%rax
118b: 48 8d 15 72 0e 00 00 lea 0xe72(%rip),%rdx # 2004 <_IO_stdin_used+0x4>
1192: 48 89 d6 mov %rdx,%rsi
1195: 48 89 c7 mov %rax,%rdi
1198: e8 d3 fe ff ff call 1070 <strcmp@plt>
119d: 85 c0 test %eax,%eax
119f: 0f 94 c0 sete %al
11a2: 0f b6 c0 movzbl %al,%eax
11a5: c9 leave
11a6: c3 ret

我们在同一环境对 O3 优化等级做相同的实验,得到了相同的结果:

lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O3
ub_null.c: In function ‘is_valid’:
ub_null.c:6:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
6 | if(*str == 0x80) return 0;
| ^~
lyazj@HelloWorld:~$ ./ub_null
0
lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null ub_null: file format elf64-x86-64 Disassembly of section .text: 00000000000011a0 <is_valid>:
11a0: f3 0f 1e fa endbr64
11a4: 48 85 ff test %rdi,%rdi
11a7: 74 27 je 11d0 <is_valid+0x30>
11a9: 48 83 ec 08 sub $0x8,%rsp
11ad: 48 8d 35 50 0e 00 00 lea 0xe50(%rip),%rsi # 2004 <_IO_stdin_used+0x4>
11b4: e8 a7 fe ff ff call 1060 <strcmp@plt>
11b9: 85 c0 test %eax,%eax
11bb: 0f 94 c0 sete %al
11be: 48 83 c4 08 add $0x8,%rsp
11c2: 0f b6 c0 movzbl %al,%eax
11c5: c3 ret
11c6: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
11cd: 00 00 00
11d0: 31 c0 xor %eax,%eax
11d2: c3 ret

接下来我们用下面的两行代码替换被优化掉的 if 语句,看看会发生什么:

char head = *str;
if(head == 0x80) return 0;
lyazj@HelloWorld:~$ ./ub_null
Segmentation fault
lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null ub_null: file format elf64-x86-64 Disassembly of section .text: 0000000000001169 <is_valid>:
1169: f3 0f 1e fa endbr64
116d: 55 push %rbp
116e: 48 89 e5 mov %rsp,%rbp
1171: 48 83 ec 20 sub $0x20,%rsp
1175: 48 89 7d e8 mov %rdi,-0x18(%rbp)
1179: 48 8b 45 e8 mov -0x18(%rbp),%rax
117d: 0f b6 00 movzbl (%rax),%eax
1180: 88 45 ff mov %al,-0x1(%rbp)
1183: 48 83 7d e8 00 cmpq $0x0,-0x18(%rbp)
1188: 75 07 jne 1191 <is_valid+0x28>
118a: b8 00 00 00 00 mov $0x0,%eax
118f: eb 1e jmp 11af <is_valid+0x46>
1191: 48 8b 45 e8 mov -0x18(%rbp),%rax
1195: 48 8d 15 68 0e 00 00 lea 0xe68(%rip),%rdx # 2004 <_IO_stdin_used+0x4>
119c: 48 89 d6 mov %rdx,%rsi
119f: 48 89 c7 mov %rax,%rdi
11a2: e8 c9 fe ff ff call 1070 <strcmp@plt>
11a7: 85 c0 test %eax,%eax
11a9: 0f 94 c0 sete %al
11ac: 0f b6 c0 movzbl %al,%eax
11af: c9 leave
11b0: c3 ret

段错误如愿以偿地发生了,且是来自读取 str 处 1 字节并进行零扩展的 movzbl 指令,之前看到的编译期求值没有再次发生。

现在升高优化等级至 Og,编译期求值并优化掉第一个 if 语句的特效回归了:

lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -Og
ub_null.c: In function ‘is_valid’:
ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
7 | if(head == 0x80) return 0;
| ^~
lyazj@HelloWorld:~$ ./ub_null
0
lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null ub_null: file format elf64-x86-64 Disassembly of section .text: 0000000000001169 <is_valid>:
1169: f3 0f 1e fa endbr64
116d: 48 85 ff test %rdi,%rdi
1170: 74 1d je 118f <is_valid+0x26>
1172: 48 83 ec 08 sub $0x8,%rsp
1176: 48 8d 35 87 0e 00 00 lea 0xe87(%rip),%rsi # 2004 <_IO_stdin_used+0x4>
117d: e8 de fe ff ff call 1060 <strcmp@plt>
1182: 85 c0 test %eax,%eax
1184: 0f 94 c0 sete %al
1187: 0f b6 c0 movzbl %al,%eax
118a: 48 83 c4 08 add $0x8,%rsp
118e: c3 ret
118f: b8 00 00 00 00 mov $0x0,%eax
1194: c3 ret

GCC 如何优化,除取决于编译选项外,同样取决于程序员编写什么样的源代码,这一点不足为奇。然而,当优化等级升至 O2 时,更为不好的事情发生了:

lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O2
ub_null.c: In function ‘is_valid’:
ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
7 | if(head == 0x80) return 0;
| ^~
In function ‘is_valid’,
inlined from ‘main’ at ub_null.c:15:3:
ub_null.c:9:10: warning: argument 1 null where non-null expected [-Wnonnull]
9 | return strcmp(str, "expected string") == 0;
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from ub_null.c:2:
ub_null.c: In function ‘main’:
/usr/include/string.h:156:12: note: in a call to function ‘strcmp’ declared ‘nonnull
156 | extern int strcmp (const char *__s1, const char *__s2)
| ^~~~~~
lyazj@HelloWorld:~$ ./ub_null
Segmentation fault
lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null ub_null: file format elf64-x86-64 Disassembly of section .text: 00000000000011b0 <is_valid>:
11b0: f3 0f 1e fa endbr64
11b4: 48 83 ec 08 sub $0x8,%rsp
11b8: 48 8d 35 45 0e 00 00 lea 0xe45(%rip),%rsi # 2004 <_IO_stdin_used+0x4>
11bf: e8 9c fe ff ff call 1060 <strcmp@plt>
11c4: 85 c0 test %eax,%eax
11c6: 0f 94 c0 sete %al
11c9: 48 83 c4 08 add $0x8,%rsp
11cd: 0f b6 c0 movzbl %al,%eax
11d0: c3 ret

值得注意的是,现在段错误来自 strcmp() 中的 NULL dereference,且 is_valid() 的反汇编出奇地简单,GCC 同时干掉了两个 if 语句!因为我们首先访问了 str 处的 1 字节,由于 NULL dereference 是典型的 UB,编译器便假定了 str != NULL,这样第二个 if 语句也可以被优化掉!现在,我们产生了具有严重漏洞的 is_valid() 函数,当 str == NULL 时,程序将发生严重错误。

解决 bug 的方法是显然的,即将 head 转换为 unsigned char 再比较,并调换两个 if 语句的顺序。NULL dereference,这是一个曾经让 Linux 内核爆出严重漏洞的 UB,我们刚刚成功复现了这一过程。诚然,此处的程序是异常简单的,看出两个写反的 if 语句非常容易;但对于代码业务特别复杂的场景,特别是对一行代码需要数行注释的底层代码或其它核心代码而言,这个 bug 可能一致遗留下来,并成为一个长期休眠的不定时炸弹。它的出现让许多可能是至关重要的代码段,如第二个 if 语句失效,可能给程序使用者造成难以预计的后果。还好当前版本的 GCC 友好地给出了应有的警告,这也再次向我们证明,随意地忽略编译器给出的 Warning 是不可取的。

Google 等团队开发的 sanitizer 已经集成到了当前版本的 GCC 中,让我们用 sanitizer 更为有效地发现上述未定义行为:

lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O2 -fsanitize=undefined -fno-sanitize-recover=all
ub_null.c: In function ‘is_valid’:
ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
7 | if(head == 0x80) return 0;
| ^~
lyazj@HelloWorld:~$ ./ub_null
ub_null.c:6:8: runtime error: load of null pointer of type 'const char'

看到这里,是不是很轻松就发现了两个 if 语句写反的问题?

【持续更新】C/C++ 踩坑记录(一)的更多相关文章

  1. CentOS7.4安装MySQL踩坑记录

    CentOS7.4安装MySQL踩坑记录 time: 2018.3.19 CentOS7.4安装MySQL时网上的文档虽然多但是不靠谱的也多, 可能因为版本与时间的问题, 所以记录下自己踩坑的过程, ...

  2. ubuntu 下安装docker 踩坑记录

    ubuntu 下安装docker 踩坑记录 # Setp : 移除旧版本Docker sudo apt-get remove docker docker-engine docker.io # Step ...

  3. 你真的了解字典(Dictionary)吗? C# Memory Cache 踩坑记录 .net 泛型 结构化CSS设计思维 WinForm POST上传与后台接收 高效实用的.NET开源项目 .net 笔试面试总结(3) .net 笔试面试总结(2) 依赖注入 C# RSA 加密 C#与Java AES 加密解密

    你真的了解字典(Dictionary)吗?   从一道亲身经历的面试题说起 半年前,我参加我现在所在公司的面试,面试官给了一道题,说有一个Y形的链表,知道起始节点,找出交叉节点.为了便于描述,我把上面 ...

  4. ABP框架踩坑记录

    ABP框架踩坑记录 ASP.NET Boilerplate是一个专用于现代Web应用程序的通用应用程序框架. 它使用了你已经熟悉的工具,并根据它们实现最佳实践. 文章目录 使用MySQL 配置User ...

  5. python发布包到pypi的踩坑记录

    前言 突然想玩玩python了^_^ 这篇博文记录了我打算发布包到pypi的踩坑经历.python更新太快了,甚至连这种发布上传机制都在不断的更新,这导致网上的一些关于python发布上传到pypi的 ...

  6. manjaro xfce 18.0 踩坑记录

    manjaro xfce 18.0 踩坑记录 1 简介1.1 Manjaro Linux1.2 开发桌面环境2 自动打开 NumLock3 系统快照3.1 安装timeshift3.2 使用times ...

  7. QT踩坑记录1-Q_OBJECT编译问题

    QT踩坑记录1-Q_OBJECT编译问题 QTC++Bugs 错误输出 Q_OBJECT 宏错误的地方会编译出现这样的错误, 无法找到.... 由于自己不想再看到这个错误, 此处 复制自 参考连接1, ...

  8. Spark Ignite踩坑记录

    Ignite spark 踩坑记录 简述 ignite访问数据有两种模式: Thin Jdbc模式: Jdbc 模式和Ignite client模式: shell客户端输出问题,不能输出全列: 针对上 ...

  9. DevOps落地实践点滴和踩坑记录-(2) -聊聊平台建设

    很久没有写文章记录了,上一篇文章像流水账一样,把所见所闻一个个记录下来.这次专门聊聊DevOps平台的建设吧,有些新的体会和思考,希望给正在做这个事情的同学们一些启发吧. DevOps落地实践点滴和踩 ...

  10. 【Unity】2021接Bugly踩坑记录

    写在前面 因为在工作项目中用到Bugly,所以我在自己的测试工程中尝试接入Bugly,却没有成功,明明一切是按照说明书操作,为什么会不成功?当时在网上找了很久的资料,最后试成功了,这里把当时遇到的问题 ...

随机推荐

  1. vue中使用svg并设置大小

    1.安装依赖 npm install --save-dev svg-sprite-loader 2. 新建svg资源文件夹     src/assets/svg 将svg资源放入此目录,接下来会在配置 ...

  2. [双目视差] 单双目MATLAB 相机标定(二)双目摄像机标定

    文章目录 单双目MATLAB 相机标定(二)双目摄像机标定 一.环境准备 二.标定过程 单双目MATLAB 相机标定(二)双目摄像机标定 一.环境准备 MATLAB R2014a+windows7 6 ...

  3. Jupyter Notebook(或vscode插件) 一个cell有多个输出

    方法一 在文件的开头加上如下代码,该方法仅对当前文件有效 from IPython.core.interativeshell import InteractiveShell InteractiveSh ...

  4. 用Python语言进行时间序列ARIMA模型分析

    应用时间序列 时间序列分析是一种重要的数据分析方法,应用广泛.以下列举了几个时间序列分析的应用场景: 1.经济预测:时间序列分析可以用来分析经济数据,预测未来经济趋势和走向.例如,利用历史股市数据和经 ...

  5. 2020-12-09:TCP中,慢启动是什么?

    福哥答案2020-12-09: 简单回答:乘法增大. 中级回答:cwnd拥塞窗口.rwnd接收窗口.MSS最大报文段长度.ssthresh慢开始门限.使用慢开始算法后,每经过一个传输轮次(即往返时延R ...

  6. Python-查询所有python版本

    C:\Users\liujun>where pythonD:\Python\Python310\python.exeD:\Python\Python38\python.exeC:\Users\l ...

  7. hasattr()、getattr()、setattr()函数简介

    hasattr(object, name) 判断object对象中是否存在name属性,当然对于python的对象而言,属性包含变量和方法:有则返回True,没有则返回False:需要注意的是name ...

  8. Django admin管理工具的使用、定制及源码解析

    admin组件使用 Django 提供了基于 web 的管理工具. Django 自动管理工具是 django.contrib 的一部分.你可以在项目的 settings.py 中的 INSTALLE ...

  9. django之drf(部分讲解)

    序列化类常用字段和字段参数 drf在Django字段类型的基础上派生了自己的字段类型以及字段参数 序列化器的字段类型用于处理原始值和内部数据类型直接的转换 还可以用于验证输入.以及父对象检索和设置值 ...

  10. 02、SECS-II 通信协议介绍

    这里我们先学习 SECS-II 协议,给我的感受是先学完 SECS-II 协议,再去学习 SECS-I 和 HSMS 协议更加容易理解,所以这里我先介绍 SECS-II 协议. 文章的内容基本上来自参 ...