未定义行为之 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. 第6章. 部署到GithubPages

    依托GitHub Pages 服务,可以把 vuepress 编译后的 博客静态文件 放置到该平台,那么就可以把静态页面发布出来,就会实现了不用购买云服务器就可以发布静态页面的功能. 1. 创建仓库 ...

  2. 【Visual Leak Detector】在 VS 高版本中使用 VLD

    说明 使用 VLD 内存泄漏检测工具辅助开发时整理的学习笔记. 本篇介绍如何在 VS 高版本中使用 vld2.5.1.同系列文章目录可见 <内存泄漏检测工具>目录 目录 说明 1. 使用前 ...

  3. AI天后,在线飙歌,人工智能AI孙燕姿模型应用实践,复刻《遥远的歌》,原唱晴子(Python3.10)

    忽如一夜春风来,亚洲天后孙燕姿独特而柔美的音色再度响彻华语乐坛,只不过这一次,不是因为她出了新专辑,而是人工智能AI技术对于孙燕姿音色的完美复刻,以大江灌浪之势对华语歌坛诸多经典作品进行了翻唱,还原度 ...

  4. Grafana系列-统一展示-11-Logs Traces无缝跳转

    系列文章 Grafana 系列文章 概述 如前文 Grafana 系列 - 统一展示 -1- 开篇所述, Grafana 可以了解所有相关的数据--以及它们之间的关系--对于尽快根治事件和确定意外系统 ...

  5. 设置nginx允许服务端跨域

    目前项目大多使用前后端分离的模式进行开发,跨域请求当然就是必不可少了,很多时候我们会使用在客户端的ajax 请求中设置跨域请求,也有的在服务端设置跨域.但是有时候会遇到不使用ajax也没有使用后端服务 ...

  6. vue项目提示TypeError: e.call is not a function

    最近运行vue项目老是提示TypeError: e.call is not a function 如上一运行到该页面就会提示这个错误,虽然页面功能都没受到影响,但是这个必定会给后期发布后的项目带来极大 ...

  7. Docker 镜像命令

    Docker 镜像命令 1.Docker images--列出本地镜像 命令:docker images [OPTIONS] [REPOSITORY[:TAG]] 选项 -a :列出本地所有的镜像(含 ...

  8. ics-05

    挺有意思的一题 攻防世界->web->ics-05 打开题目链接,就是一个很正常的管理系统,只有左侧的可以点着玩 并且点到**设备维护中心时,页面变为index.php 查看响应 发现云平 ...

  9. 高精度减法(模板yxc)

    #include <bits/stdc++.h> using namespace std; bool cmp(vector<int> &A, vector<int ...

  10. 使用containerd从0搭建k8s(kubernetes)集群

    准备环境 准备两台服务器节点,如果需要安装虚拟机,可以参考<wmware和centos安装过程> 机器名 IP 角色 CPU 内存 centos01 192.168.109.130 mas ...